Silverlight: Product Maintenance Application ( Part 3 – Getting some data )

Following up on this previous post I wanted to start building out some classes that managed data for me “in front” of the services I’d built.

I figured that I needed data for my chart, data for my products and reference data and so I sketched out a “DataModel” class;

public class DataModel : PropertyChangeNotifier{  public DataModel()  {    salesChartData = new ChartData();    referenceData = new ReferenceData();    productData = new ProductData();  }  public ChartData SalesChartData  {    get    {      return (salesChartData);    }  }  public ReferenceData ReferenceData  {    get    {      return (referenceData);    }  }  public ProductData ProductData  {    get    {      return (productData);    }  }  ChartData salesChartData;  ReferenceData referenceData;  ProductData productData;}

and I want to provide this ( or pieces of it such as DataModel.SalesChartData to various controls throughout the app so that they can data-bind to it ).

There seemed to be a bit of commonality in the various classes I wanted to write in that each of them does an asynchronous load and then fires some events to say they’re done so I stuck in a base class ( not yet sure about the constraint on T here as I’m not sure whether it’s a good idea to pass a specific derived EventArgs class through to the FireEventsOnAsyncResult method );

public abstract class GenericDataSource<T> : PropertyChangeNotifier where T : AsyncCompletedEventArgs{  public event EventHandler LoadBeginning;  public event EventHandler<T> LoadSucceeded;  public event EventHandler<T> LoadFailed;  public bool IsLoading  {    get    {      return (isLoading);    }    set    {      isLoading = value;      FirePropertyChanged("IsLoading");    }  }  public void LoadAsync()  {    FireLoadBeginning();    IsLoading = true;    OnLoadAsync();  }  public abstract void OnLoadAsync();  protected bool CheckResult(T args)  {          return ((args.Error == null) && (!args.Cancelled));  }  protected void FireEventsOnAsyncResult(T args)  {    if (CheckResult(args))    {      FireLoadSucceeded(args);    }    else    {      FireLoadFailed(args);    }    IsLoading = false;  }  protected void FireLoadSucceeded(T t)  {    if (LoadSucceeded != null)    {      LoadSucceeded(this, t);    }  }  protected void FireLoadFailed(T t)  {    if (LoadFailed != null)    {      LoadFailed(this, t);    }  }  protected void FireLoadBeginning()  {    if (LoadBeginning != null)    {      LoadBeginning(this, null);    }  }  bool isLoading;}

note – this base class isn’t quite right to me because it offers the implementor of the concrete class that derives from it the opportunity not to call CheckResult and not to call FireEventsOnAsyncResult which I think is a bit of a failing but it’s only a sample app so I’ve not addressed that yet. I went ahead and used it as the basis for my ReferenceData class though;

public class ReferenceData : GenericDataSource<AsyncCompletedEventArgs>{  internal ReferenceData()  {    referenceData = new Dictionary<string, ObservableCollection<LookupDataItem>>();  }  public IEnumerable<LookupDataItem> Categories  {    get    {      IEnumerable<LookupDataItem> retVal = null;      if (referenceData.ContainsKey("categories"))      {        retVal = referenceData["categories"];      }      return (retVal);    }  }  public IEnumerable<LookupDataItem> Suppliers  {    get    {      IEnumerable<LookupDataItem> retVal = null;      if (referenceData.ContainsKey("suppliers"))      {        retVal = referenceData["suppliers"];      }      return (retVal);    }  }  public override void OnLoadAsync()  {    if (!loaded)    {      ProductServiceClient proxy = new ProductServiceClient();      proxy.GetReferenceDataCompleted += (s, e) =>        {          loaded = true;          if (CheckResult(e))          {            referenceData = e.Result;            FirePropertyChanged("Categories");            FirePropertyChanged("Suppliers");          }          FireEventsOnAsyncResult(e);        };      proxy.GetReferenceDataAsync();    }    else    {      FireLoadSucceeded(new AsyncCompletedEventArgs(null, false, null));    }  }  bool loaded;  Dictionary<string, ObservableCollection<LookupDataItem>> referenceData;}

and so the idea now is that a caller can do something like;

DataModel model = new DataModel();

model.ReferenceData.LoadFailed += SomeHandler;

model.ReferenceData.LoadAsync();

SomeControl.DataContext = model.ReferenceData;

or that kind of thing. I wanted to use this in a DataGrid so that I could display a ComboBox with the idea being that the ComboBox ItemsSource comes from one place (the reference data) but the value for the ComboBox comes from another. Now, in WPF that’d be fine as the WPF ComboBox has facilities to support that. But in Silverlight there’s no SelectedValue on ComboBox to make that happen and so I ended up looking at this article and building my own slightly hacky ComboBox to add that functionality. Take great care if you copy this as I’ve only tested that “it works for me some of the time” :-);

// Taken from http://www.engineserver.com/silverlightcombobox/// With a tweak or two from mtaulty - not sure it caters for everything being// updated (e.g. the items)// Makes a ComboBox more like a WPF ComboBox.namespace Controls{  public class WpfComboBox : ComboBox  {    public static readonly DependencyProperty SelectedValuePathProperty =      DependencyProperty.Register("SelectedValuePath", typeof(string), typeof(WpfComboBox),      new PropertyMetadata(new PropertyChangedCallback(SelectedValuePathPropertyChanged)));    public static readonly DependencyProperty SelectedValueProperty =      DependencyProperty.Register("SelectedValue", typeof(object), typeof(WpfComboBox),        new PropertyMetadata(new PropertyChangedCallback(OnSelectedValueChanged)));    public WpfComboBox()    {      base.SelectionChanged += OnSelectionChanged;    }    void SetValueFromSelection()    {      if (!string.IsNullOrEmpty(SelectedValuePath) && (SelectedItem != null))      {        object value = SelectedItem.GetType().GetProperty(          SelectedValuePath).GetValue(SelectedItem, null);        SetValue(WpfComboBox.SelectedValueProperty, value);      }    }    void SetSelectionFromValue()    {      object item = null;      if ((Items != null) && !string.IsNullOrEmpty(SelectedValuePath))      {        item =          (            from i in Items            where i.GetType().GetProperty(SelectedValuePath).GetValue(i, null).Equals(SelectedValue)            select i          ).SingleOrDefault();      }      SelectedItem = item;    }    void OnSelectionChanged(object sender, SelectionChangedEventArgs e)    {      SetValueFromSelection();    }    static void OnSelectedValueChanged(DependencyObject sender,      DependencyPropertyChangedEventArgs args)    {      ((WpfComboBox)sender).SetSelectionFromValue();    }    static void SelectedValuePathPropertyChanged(DependencyObject sender,      DependencyPropertyChangedEventArgs args)    {      ((WpfComboBox)sender).SetSelectionFromValue();    }    public string SelectedValuePath    {      get      {        return (string)GetValue(WpfComboBox.SelectedValuePathProperty);      }      set      {        SetValue(WpfComboBox.SelectedValuePathProperty, value);      }    }    public object SelectedValue    {      get      {        return GetValue(SelectedValueProperty);      }      set      {        base.SetValue(SelectedValueProperty, value);      }    }  }}

Now, combining that ComboBox with my reference data I can use it like this;

<local:WpfComboBox

    ItemsSource="{Binding Source={StaticResource referenceDataSource},Path=DataContext.ReferenceData.Suppliers}"

    DisplayMemberPath="Title"

    SelectedValuePath="Id"

    SelectedValue="{Binding SupplierID,Mode=TwoWay}" />

where you can see that the ItemsSource of the ComboBox is coming from the Suppliers property on my ReferenceData whereas the value itself is coming from a SupplierID value that we’re bound to elsewhere. This is a common enough thing to want to do and I did a similar thing in WPF here so the only change here is that SelectedValue needed sorting out.

With my reference data out of the way, I wanted to sort out my ChartData which is pretty simple/similar and ends up looking like;

public class ChartData : GenericDataSource<GetProductSalesCompletedEventArgs>{  internal ChartData()  {  }  public int ProductId  {    get    {      return (productId);    }    set    {      productId = value;      FirePropertyChanged("ProductId");    }  }  public string ProductName  {    get    {      return (productName);    }    set    {      productName = value;      FirePropertyChanged("ProductName");    }  }     public ObservableCollection<ProductSales> ProductSales  {    get    {      return (productSales);    }    set    {      productSales = value;      FirePropertyChanged("ProductSales");    }  }  public override void OnLoadAsync()  {    ProductServiceClient proxy = new ProductServiceClient();          proxy.GetProductSalesCompleted += (s, e) =>      {        if (CheckResult(e))        {          ProductName = productName;          ProductSales = e.Result;        }        FireEventsOnAsyncResult(e);      };    proxy.GetProductSalesAsync(productId);  }   ObservableCollection<ProductSales> productSales;  string productName;  int productId;}

and so it’s just a matter of the caller setting 2 properties for ProductId and ProductName and then calling LoadAsync to get the class to go off and call the web service and update properties that can be bound to. You could argue that I shouldn’t really change those properties before going off to call the web-service because they stay “changed” in the face of web service failures but I can live with that for my sample.

The final bit of data that I need to deal with is the ProductData which is a more complex beast.

What makes ProductData more complicated is that it needs to deal with;

  1. Searching
  2. Paging
  3. Updating
  4. Inserting

and so it’s more than just “load and forget”. By far the most complex part of this for me is dealing with updating because my ProductData class wants to be able to hand out a collection of Product to be data-bound and then those products can be edited and I need to track;

  1. The changes made to those Products.
  2. The original value of those Products in order to submit both those and the associated changes back to the web-service. In my previous post the web-service interface for updates makes use of  a ProductChangeEntry { OriginalProduct, CurrentProduct } which pushes the requirement to the client to keep track of these things.

So…I copy the Product values as they come back from the web-service and ( thanks to the WCF code-generation bits ) they implement INotifyPropertyChanged so I can be aware when the UI makes changes to them.

With all that said, my ProductData class ends up looking like this;

public class ProductSavedEventArgs : EventArgs{  public int RecordsAttempted   {     get;     internal set;   }  public int RecordsFailed   {     get;     internal set;   }}public class ProductData : GenericDataSource<AsyncCompletedEventArgs>{  public event EventHandler<ProductSavedEventArgs> SaveCompleted;  public ProductData()  {    searchTerm = new SearchParameters()    {      Page = 0,      PageSize = 10    };    searchToken = new SearchResultToken();    products = new ObservableCollection<Product>();    changedProducts = new ObservableCollection<ProductChangeEntry>();    insertedProducts = new ObservableCollection<Product>();  }  public void AddProduct()  {    Product p = new Product();    insertedProducts.Add(p);    Products.Add(p);    FirePropertyChanged("HasChangedProducts");  }  public SearchParameters SearchTerm  {    get    {      return (searchTerm);    }    set    {      searchTerm = value;      FirePropertyChanged("SearchTerm");    }  }  public SearchResultToken SearchResultDetails  {    get    {      return (searchToken);    }    internal set    {      searchToken = value;      FirePropertyChanged("SearchResultDetails");    }  }  public ObservableCollection<Product> Products  {    get    {      return (products);    }    internal set    {      products = value;      FirePropertyChanged("Products");      FirePropertyChanged("HasData");    }  }  internal ObservableCollection<ProductChangeEntry> ChangedProductEntries  {    get    {      ObservableCollection<ProductChangeEntry> entries = new ObservableCollection<ProductChangeEntry>();      foreach (ProductChangeEntry entry in changedProducts)      {        if (entry.CurrentProduct != null)        {          entries.Add(entry);        }      }      return (entries);    }  }  public bool HasChangedProducts  {    get    {      return ((insertedProducts.Count > 0) ||        changedProducts.Count(p => p.CurrentProduct != null) > 0);    }  }  public bool HasData  {    get    {      return (Products.Count > 0);    }  }  public override void OnLoadAsync()  {    ProductServiceClient proxy = new ProductServiceClient();    proxy.GetProductsCompleted += (s, e) =>    {      if (CheckResult(e))      {        SearchResultDetails.Page = e.Result.Page + 1;        SearchResultDetails.TotalPages = e.Result.TotalPages;        ResetProductLists();        Products = e.Result.Products;        CreateProductLists();      }      FireEventsOnAsyncResult(e);    };    proxy.GetProductsAsync(SearchTerm.Page, SearchTerm.PageSize,      SearchTerm.SearchTerm);  }  public void NextPage()  {    searchTerm.Page++;    LoadAsync();  }  public void PreviousPage()  {    searchTerm.Page--;    LoadAsync();  }  public void SaveChangesAsync()  {    if (HasChangedProducts)    {      IsLoading = true;      ProductSavedEventArgs args = new ProductSavedEventArgs();      if (insertedProducts.Count > 0)      {        ProductServiceClient proxy = new ProductServiceClient();        args.RecordsAttempted = insertedProducts.Count;        proxy.InsertProductsCompleted += (s, e) =>          {            if ((e.Error != null) || (e.Cancelled))            {              args.RecordsFailed += insertedProducts.Count;            }            else            {              insertedProducts = new ObservableCollection<Product>();              FirePropertyChanged("HasChangedProducts");            }            UpdateRecordsAsync(args);          };        proxy.InsertProductsAsync(insertedProducts);      }      else      {        UpdateRecordsAsync(args);      }    }  }  void UpdateRecordsAsync(ProductSavedEventArgs args)  {    ProductServiceClient proxy = new ProductServiceClient();    if (ChangedProductEntries.Count > 0)    {      args.RecordsAttempted += ChangedProductEntries.Count;      proxy.UpdateProductsCompleted += (s, e) =>      {        if ((e.Error != null) || (e.Cancelled))        {          args.RecordsFailed += args.RecordsAttempted;        }        else        {          args.RecordsFailed += ChangedProductEntries.Count - e.Result.Count;          foreach (int id in e.Result)          {            ProductChangeEntry entry = changedProducts.Single(              p => p.OriginalProduct.ProductID == id);            entry.CurrentProduct = null;          }          FirePropertyChanged("HasChangedProducts");        }        if (SaveCompleted != null)        {          SaveCompleted(this, args);        }        IsLoading = false;      };      proxy.UpdateProductsAsync(ChangedProductEntries);    }    else    {      if (SaveCompleted != null)      {        SaveCompleted(this, args);      }      IsLoading = false;    }  }  void ResetProductLists()  {    if (Products != null)    {      changedProducts = new ObservableCollection<ProductChangeEntry>();      insertedProducts = new ObservableCollection<Product>();      foreach (Product p in Products)      {        p.PropertyChanged -= OnProductPropertyChanged;               }    }  }  void CreateProductLists()  {    if (Products != null)    {      foreach (Product p in Products)      {        p.PropertyChanged += OnProductPropertyChanged;        changedProducts.Add(new ProductChangeEntry()        {          CurrentProduct = null,          OriginalProduct = new Product()          {            CategoryID = p.CategoryID,            Discontinued = p.Discontinued,            ProductID = p.ProductID,            ProductName = p.ProductName,            QuantityPerUnit = p.QuantityPerUnit,            ReorderLevel = p.ReorderLevel,            SupplierID = p.SupplierID,            UnitPrice = p.UnitPrice,            UnitsInStock = p.UnitsInStock,            UnitsOnOrder = p.UnitsOnOrder          }        });      }    }  }  void OnProductPropertyChanged(object sender, PropertyChangedEventArgs args)  {    Product changedProduct = (Product)sender;    ProductChangeEntry entry = changedProducts.SingleOrDefault(      p => p.OriginalProduct.ProductID == changedProduct.ProductID);    if (entry.CurrentProduct == null)    {      entry.CurrentProduct = changedProduct;      FirePropertyChanged("HasChangedProducts");    }  }  SearchParameters searchTerm;  SearchResultToken searchToken;  ObservableCollection<Product> products;  ObservableCollection<ProductChangeEntry> changedProducts;  ObservableCollection<Product> insertedProducts;}

and so there’s perhaps more lists kicking around than I’d really like and if I was doing this for many tables then I’d need to come up with a way to either;

  1. Make this generic in some fashion.
  2. Have this kind of code (or at least most of it) generated by a tool.

as I’m happy to write this once but I wouldn’t want to write it more than once 🙂

Ok, I’ve now got some data classes…next step is to put some controls on top of these…