Silverlight and ADO.NET Data Services ( 2 )

Following on from this last post, if I start to edit the data in the grid and change some value then the property value will change in the underlying class but nothing else will happen. That is, the NorthwindEntities class which is managing my data for me client-side ( which is derived from DataServiceContext ) isn’t going to be aware of those modifications.

Why not? If we have a look at a property on a generated class such as this one for the CompanyName on the Customers class;

        public string CompanyName
        {
            get
            {
                return this._CompanyName;
            }
            set
            {
                this.OnCompanyNameChanging(value);
                this._CompanyName = value;
                this.OnCompanyNameChanged();
            }
        }

Now, OnCompanyNameChanging and OnCompanyNameChanged are partial methods with no implementation by default so nothing’s going to happen when something like the DataGrid changes a value such as CompanyName.

Similarly, the generated entity classes such as Customers do not implement INotifyPropertyChanged which means that changes in the data will not be reflected in the UI anywhere as the UI doesn’t have the necessary events to sync up to. This would also be true of an ADO.NET Data Services client generated for the full .NET framework ( such as WPF ) right now as it’s the same tool that’s in use.

( It wouldn’t be true if you were using generated code from the Entity Framework directly in your UI ( i.e. a 2-tier solution ) because the Entity Framework ObjectContext and generated entity classes do set up a relationship where one “tracks” the other via the ObjectContext’s ObjectStateManager and the IEntityWithChangeTracker interface and EntityObject’s implementation of it ).

So, we’re not going to get automatic, 2-way databinding on these entity classes just yet ( I’m hoping that RTM might do that but I’ve no inside knowledge on that ) and there’s not really any way that you want to try and manually add it if you’ve got a lot of generated classes with a lot of generated properties – way too much manual work there and ( AFAIK ) datasvcutil.exe isn’t extensible.

So you might want to look at something like Josh’s script over here which does a crafty search-and-replace for you.

Regardless, if I edit data in my DataGrid as I’ve got it currently then the edits will make it through to the underlying property on the data bound object. So, if I change my UI to add a “Save” button;

<UserControl
  x:Class="BlogPostSL.Page"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data">
  <Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
      <RowDefinition
        Height="8*" />
      <RowDefinition />
      <RowDefinition />
      <RowDefinition />
    </Grid.RowDefinitions>
    <data:DataGrid
      x:Name="dataGrid"
      Margin="10"
      AutoGenerateColumns="True"
      ItemsSource="{Binding}" />
    <Button
      Margin="10"
      Content="Get Data"
      Grid.Row="1"
      Click="OnGetData" />
    <Button
      Margin="10"
      Content="Save Data"
      Grid.Row="2"
      Click="OnSaveData" />
  </Grid>
</UserControl>

and then add a little code to my code-behind class ( I’ve not included the whole class here, just the new method );

 void OnSaveData(object sender, EventArgs args)
    {
      proxy.BeginSaveChanges((asyncResult) =>
        {
          try
          {
            proxy.EndSaveChanges(asyncResult);
          }
          catch (Exception ex)
          {
            // TODO
            Debugger.Break();
          }
        }, null);
    }

Then nothing happens when that code inside my OnSaveData function runs. If I peer inside the proxy instance a little bit with the debugger then I can see a bunch of my Customers entities on a list of EntityDescriptor with a status of Unchanged. So, we need to make sure that our grid is setting the right instance to be updated when it has been updated as in;

    public Page()
    {
      InitializeComponent();

      this.Loaded += OnLoaded;

      dataGrid.AutoGeneratingColumn += OnGeneratedColumn;
      dataGrid.CommittingEdit += OnCommittingEdit;
    }
    void OnCommittingEdit(object sender, DataGridEndingEditEventArgs e)
    {
      proxy.UpdateObject(e.Row.DataContext);
    }

Then, when I update data and click my Save button then I get my data saved back into the database. Naturally, this is being done (AFAIK!) in line with the concurrency options and if I wanted to I could look to do batch updates rather than individual PUTs to make it happen as in;

    void OnSaveData(object sender, EventArgs args)
    {
      proxy.BeginSaveChanges(SaveChangesOptions.Batch, (asyncResult) =>
        {
          try
          {
            proxy.EndSaveChanges(asyncResult);
          }
          catch (Exception ex)
          {
            // TODO
            Debugger.Break();
          }
        }, null);
    }

I can leave that in place and handle deletes by, for instance, trapping the delete keypress on a grid row and then making a call to DeleteObject. I might do that by the following ( again, only listed the added code not the whole code for my Page class );

  public partial class Page : UserControl
  {
    private bool inEdit;

    public Page()
    {
      InitializeComponent();

      this.Loaded += OnLoaded;

      dataGrid.AutoGeneratingColumn += OnGeneratedColumn;
      dataGrid.BeginningEdit += OnBeginningEdit;
      dataGrid.CommittingEdit += OnCommittingEdit;
      dataGrid.CancelingEdit += OnCancellingEdit;
      dataGrid.KeyDown += OnGridKeyDown;
    }
    void OnCancellingEdit(object sender, DataGridEndingEditEventArgs e)
    {
      inEdit = false;
    }
    void OnBeginningEdit(object sender, DataGridBeginningEditEventArgs e)
    {
      inEdit = true;
    }
    void OnCommittingEdit(object sender, DataGridEndingEditEventArgs e)
    {
      proxy.UpdateObject(e.Row.DataContext);
    }
    void OnGridKeyDown(object sender, KeyEventArgs e)
    {
      if ((e.Key == Key.Delete) && (!inEdit))
      {
        // Single select for me.
        if (dataGrid.SelectedItem != null)
        {
          // Tell the DataServiceContext
          proxy.DeleteObject(dataGrid.SelectedItem);

          // Remove from the bound collection, disappears from
          // grid.
          BoundData.Remove(dataGrid.SelectedItem as Customers);
        }
      }
    }
    ObservableCollection<Customers> BoundData
    {
      get
      {
        return (dataGrid.DataContext as ObservableCollection<Customers>);
      }
    }

So, I’ve a form of update and a form of delete working. Insert can maybe be handlded by just pressing the Insert key, changing that handler to;

    void OnGridKeyDown(object sender, KeyEventArgs e)
    {
      if (!inEdit)
      {
        if (e.Key == Key.Delete)
        {
          // Single select for me.
          if (dataGrid.SelectedItem != null)
          {
            // Tell the DataServiceContext
            proxy.DeleteObject(dataGrid.SelectedItem);

            // Remove from the bound collection, disappears from
            // grid.
            BoundData.Remove(dataGrid.SelectedItem as Customers);
          }
        }
        else if (e.Key == Key.Insert)
        {
          Customers c = new Customers() { Country = "UK" };
          int index = BoundData.IndexOf(dataGrid.SelectedItem as Customers);
          BoundData.Insert(index, c);
          proxy.AddObject("Customers", c);
        }
      }
    }

From there I think the next steps would be to think about inserting related entities ( 1 to many and many-to-many ), checking status codes, handling errors and so on.