-
Notifications
You must be signed in to change notification settings - Fork 2
02 Saving from client
In the previous workshop, you
saw how to query our entities from the client.
Now you will see how to save our changes.
Create a new window CustomerWindow.
For this, right click on the client project / Add / Window...
Then type CustomerWindow and click on Add
Now create a new class CustomerWindowViewModel and use WAQS context menu to init this ViewModel and do not associate it to any window.
Then update the class to use this code:
public class CustomerWindowViewModel : ViewModelBase
{
private INorthwindClientContext _context;
private CustomerWindow _customerWindow; // using the window here is not good for an MVVM point of view
public CustomerWindowViewModel(INorthwindClientContext context, string customerId, CustomerWindow customerWindow): base (context)
{
_context = context;
_customerWindow = customerWindow;
LoadCustomer(customerId).ConfigureAwait(true);
}
private Customer _customer;
public Customer Customer
{
get { return _customer; }
private set
{
_customer = value;
NotifyPropertyChanged.RaisePropertyChanged(nameof(Customer));
}
}
private async Task LoadCustomer(string customerId)
{
Customer = await _context.Customers.AsAsyncQueryable().FirstOrDefault(c => c.Id == customerId).ExecuteAsync();
}
private RelayCommand _saveCommand;
public ICommand SaveCommand
{
get { return _saveCommand ?? (_saveCommand = new RelayCommand(() => SaveChangesAsync().ConfigureAwait(true))); }
}
private async Task SaveChangesAsync()
{
try
{
await _context.SaveChangesAsync();
_customerWindow.Close();
}
catch (Exception e)
{
System.Windows.MessageBox.Show(e.Message); // this is not good for an MVVM point of view
}
}
private RelayCommand _cancelCommand;
public ICommand CancelCommand
{
get { return _cancelCommand ?? (_cancelCommand = new RelayCommand(() => _customerWindow.Close())); }
}
}
Saving changes is as easy as you probably expect it: as with Entity Framework, if your entity in link to a client context, its changes will be automatically tracked and you have nothing special to do for it.
Note that WAQS asynchronous LINQ, the FirstOrDefault method (as any other scalar methods) does not run the query as it does on "standard" LINQ. You have to use ExecuteAsync then.
We will see later that it's more interesting with this way.
Now in CustomerWindow.xaml, replace the default Grid by this code:
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center"
Margin="5"
Text="Company name" />
<TextBox Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Customer.CompanyName}" />
<TextBlock Grid.Row="1"
Margin="5"
VerticalAlignment="Center"
Text="Contact title" />
<TextBox Grid.Row="1"
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Customer.ContactTitle}" />
<TextBlock Grid.Row="2"
Margin="5"
VerticalAlignment="Center"
Text="Contact name" />
<TextBox Grid.Row="2"
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Customer.ContactName}" />
<TextBlock Grid.Row="3"
Margin="5"
VerticalAlignment="Center"
Text="Address" />
<TextBox Grid.Row="3"
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Customer.Address}" />
<TextBlock Grid.Row="4"
Margin="5"
VerticalAlignment="Center"
Text="City" />
<TextBox Grid.Row="4"
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Customer.City}" />
<TextBlock Grid.Row="5"
Margin="5"
VerticalAlignment="Center"
Text="Region" />
<TextBox Grid.Row="5"
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Customer.Region}" />
<TextBlock Grid.Row="6"
Margin="5"
VerticalAlignment="Center"
Text="Postal code" />
<TextBox Grid.Row="6"
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Customer.PostalCode}" />
<TextBlock Grid.Row="7"
Margin="5"
VerticalAlignment="Center"
Text="Country" />
<TextBox Grid.Row="7"
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Customer.Country}" />
</Grid>
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="Save"
Width="50"
Margin="5"
Command="{Binding SaveCommand}" />
<Button Content="Cancel"
Width="50"
Margin="5"
Command="{Binding CancelCommand}" />
</StackPanel>
</Grid>
In the MainWindowViewModel, add a command to edit a customer
private RelayCommand _editCommand;
public ICommand EditCommand
{
get
{
return _editCommand ?? (_editCommand = new RelayCommand(c =>
{
var customerWindow = new CustomerWindow(); // this is not good for an MVVM point of view
var customerWindowViewModel = new CustomerWindowViewModel(_context, ((CustomerInfo)c).Id, customerWindow);
customerWindow.DataContext = customerWindowViewModel;
customerWindow.Show();
}));
}
}
Then in the MainWindow, add a button Refresh and add an edit button in the ListBox ItemTemplate
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Button x:Name="Refresh"
Content="Refresh"
Height="20"
Click="RefreshClick"/>
<ListBox Grid.Row="1"
ItemsSource="{Binding Customers}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Margin="5,0"
Text="{Binding TotalSpent}" />
<Button Content="Edit"
Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
Last point, add this method to the MainWindow.xaml.cs. It will be called after clicking on the new Refresh button
private async void RefreshClick(object sender, RoutedEventArgs e)
{
Cursor = Cursors.Wait;
await ((MainWindowViewModel)DataContext).LoadCustomersAsync();
Cursor = Cursors.Arrow;
}
Now you are almost done but you have a bug!
Indeed, if you cancel a customer window and then save a new one, you will save the cancelled changes. In the same way, if you open many customer windows and save one of them, you will also save the changes made in the other one.
To avoid this, the easiest way is to use a specific context per window.
For this, just add a factory in the MainWindowViewModel constructor and save it in a private field.
private Func<INorthwindClientContext> _contextFactory;
public MainWindowViewModel(INorthwindClientContext context, Func<INorthwindClientContext> contextFactory) : base(context)
{
_context = context;
_contextFactory = contextFactory;
}
Don't worry about this new constructor parameter, it will be resolve by Unity.
Then, on customerWindowViewModel instanciation, use the factory.
var customerWindowViewModel = new CustomerWindowViewModel(_contextFactory(), ((CustomerInfo)c).Id, customerWindow);
Here we are. you can now update your customer and save changes.