Skip to content

02 Saving from client

Matthieu MEZIL edited this page May 14, 2015 · 11 revisions

Note that many pieces of code in this workshop are not good for an MVVP point of view. I assume it for the demo and I will show you in a future workshop how to refactoring this code to be in accordance with MVVM pattern.

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 in a future workshop why 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.

Clone this wiki locally