Reply to comment
VM Workshop
Craig Shoemaker has created a reference application for the MVVM pattern called VM Workshop. The idea behind a reference application is to have one application written with different technologies so that they can be compared with one another. Each incarnation of the reference application is called a reference implementation. Craig has created six such reference implementations to demonstrate different UI technologies: Silverlight, WPF, Webforms, Winforms, ASP.NET MVC, and Ajax.
To support this impressive effort, I humbly contribute the Update Controls reference implementations. Let me start with the WPF implementation. Download the source and follow along.
Independent sentries
I started with Craig’s WPF reference implementation, then modified code as necessary to demonstrate the Update Controls way. The first change was to add Independent sentries to the data model:
public class Product { /* Primary key */ public int ProductId { get; set; } /* Data members */ private string _title; private string _description; private int _quantityOnHand; private double _price; private DateTime _releaseDate; #region Independent properties // Generated by Update Controls -------------------------------- private Independent _indTitle = new Independent(); private Independent _indDescription = new Independent(); private Independent _indQuantityOnHand = new Independent(); private Independent _indPrice = new Independent(); private Independent _indReleaseDate = new Independent(); public string Title { get { _indTitle.OnGet(); return _title; } set { _indTitle.OnSet(); _title = value; } } public string Description { get { _indDescription.OnGet(); return _description; } set { _indDescription.OnSet(); _description = value; } } public int QuantityOnHand { get { _indQuantityOnHand.OnGet(); return _quantityOnHand; } set { _indQuantityOnHand.OnSet(); _quantityOnHand = value; } } public double Price { get { _indPrice.OnGet(); return _price; } set { _indPrice.OnSet(); _price = value; } } public DateTime ReleaseDate { get { _indReleaseDate.OnGet(); return _releaseDate; } set { _indReleaseDate.OnSet(); _releaseDate = value; } } // End generated code -------------------------------- #endregion /* Audit members */ public DateTime CreatedOn { get; set; } public string CreatedBy { get; set; } public DateTime ModifiedOn { get; set; } public string ModifiedBy { get; set; } public bool IsDeleted { get; set; } }
Only the properties that the user can change are guarded with Independent sentries. The ID and the audit members are not under the user’s control.
Don’t store state in the View Model
Next the View Model got a complete overhaul. Craig’s View Model stores state, which he keeps in sync with the data model through well-orchestrated code. The Update Controls way, however, is to store only a reference to the data model. When the View Model needs data, it asks for it. That way there is no synchronization to orchestrate.
Take, for example, the way that Craig orchestrates the products list:
public class DemoViewModel : INotifyPropertyChanged { // Events public event PropertyChangedEventHandler PropertyChanged; // Private fields private IList<ProductListView> _products; private IProductRepository _repository; // Public properties public IList<ProductListView> Products { get { if (this._products == null) { this._products = this.GetProducts(); } return this._products; } set { if (this._products != value) { this._products = value; this.RaisePropertyChangedEvent("Products"); } } } // Constructor public DemoViewModel(IProductRepository repository) { this._repository = repository; } // Utilities private IList<ProductListView> GetProducts() { var products = this._repository.GetAll(); return Mapper.Map<IList<Product>, IList<ProductListView>>(products); } protected virtual void RaisePropertyChangedEvent(params string[] propertyNames) { if (this.PropertyChanged != null) { foreach (string name in propertyNames) { this.PropertyChanged(this, new PropertyChangedEventArgs(name)); } } } }
He keeps a list of products in the View Model, which he loads from the repository and writes back to the repository at just the right times. He uses Automapper to copy the state of a Product into a ProductListView. When the Product changes, he has to copy that state again (a feature which he has not yet added at the time of writing).
Compare this to the Update Controls way:
public class DemoViewModel { // Private fields private IProductRepository _repository; // Public properties public IEnumerable<ProductViewModel> Products { get { return _repository.GetAll() .Select(p => new ProductViewModel(p)); } } // Constructor public DemoViewModel(IProductRepository repository) { this._repository = repository; } }
When the View Model needs the list of products, it just goes to the repository and gets it. And it doesn’t copy each product into another data structure. It just wraps each product in a View Model of its own. These Product View Models similarly hold no state. They just delegate to the Product data model.
public class ProductViewModel { private Product _product; public ProductViewModel(Product product) { _product = product; } public string Title { get { return _product.Title; } set { _product.Title = value; } } public string Description { get { return _product.Description; } set { _product.Description = value; } } public int QuantityOnHand { get { return _product.QuantityOnHand; } set { _product.QuantityOnHand = value; } } public double Price { get { return _product.Price; } set { _product.Price = value; } } public DateTime ReleaseDate { get { return _product.ReleaseDate; } set { _product.ReleaseDate = value; } } public Product Product { get { return _product; } } }
The advantage of this method is that there is only one source of data: the data model. There are no intermediate copies to accidentally get out of sync. The purpose of the View Model is not to cache intermediate data. It is there to transform the data for consumption by the view.
Add a Navigation Model
Craig’s View Model has additional state to keep track of user navigation. He stores the selected product, and provides a method for use in code behind to access it.
public class DemoViewModel : INotifyPropertyChanged { // Private fields private ProductEditView _selectedProduct; // Public properties public ProductEditView SelectedProduct { get { return this._selectedProduct; } set { if (this._selectedProduct != value) { this._selectedProduct = value; this.RaisePropertyChangedEvent("SelectedProduct"); } } } // Public API methods public void GetSelectedProduct(int productId) { Product product = this._repository.GetProductById(productId); this.SelectedProduct = ProductEditView.ToProductEditView(product); this.EditFormVisibility = Visibility.Visible; this.UpdateMessageVisibility = Visibility.Collapsed; } }
The Update Controls way, on the other hand, is to move this state into a Navigation Model. Both the data grid and the edit panel data bind to the same property, so no code behind is required.
public class DemoViewModel { // Private fields private DemoNavigationModel _navigation = new DemoNavigationModel(); // Public properties public ProductViewModel SelectedProduct { get { return _navigation.SelectedProduct == null ? null : new ProductViewModel(_navigation.SelectedProduct); } set { _navigation.SelectedProduct = value == null ? null : value.Product; } } }
The Navigation Model guards this property with Independent sentries, since it is under the user’s control.
public class DemoNavigationModel { private Product _selectedProduct; #region Independent properties // Generated by Update Controls -------------------------------- private Independent _indSelectedProduct = new Independent(); public Product SelectedProduct { get { _indSelectedProduct.OnGet(); return _selectedProduct; } set { _indSelectedProduct.OnSet(); _selectedProduct = value; ProductIsUpdated = false; } } // End generated code -------------------------------- #endregion }
Change the view through Styles and DataTriggers
One final change was to the way that visibility is controlled. The edit panel should only be visible when a product is selected, and a message should be displayed when a product is saved. Craig implemented these features by data binding the Visibility property of these controls to a couple of View Model properties:
public class DemoViewModel : INotifyPropertyChanged { // Private fields private Visibility _editFormVisibility = Visibility.Collapsed; private Visibility _updateMessageVisibility = Visibility.Collapsed; // Public properties public Visibility EditFormVisibility { get { return this._editFormVisibility; } set { if (this._editFormVisibility != value) { this._editFormVisibility = value; this.RaisePropertyChangedEvent("EditFormVisibility"); } } } public Visibility UpdateMessageVisibility { get { return this._updateMessageVisibility; } set { if (this._updateMessageVisibility != value) { this._updateMessageVisibility = value; this.RaisePropertyChangedEvent("UpdateMessageVisibility"); } } } }
He sets these properties within the View Model at the appropriate times to show and hide the controls. He binds the Visibility of the controls directly to these properties.
<Grid x:Name="editGrid" Margin="5" Visibility="{Binding EditFormVisibility}"> </Grid>
An alternative is to use Styles to set properties of controls. A DataTrigger sets the control property based on the value of a View Model property.
<Window.Resources> <Style x:Key="ProductGridStyle" TargetType="Grid"> <Style.Triggers> <DataTrigger Binding="{Binding SelectedProduct}" Value="{x:Null}"> <Setter Property="Visibility" Value="Collapsed"/> </DataTrigger> </Style.Triggers> </Style> <Style x:Key="UpdateMessageStyle" TargetType="TextBlock"> <Style.Triggers> <DataTrigger Binding="{Binding IsUpdateMessageVisible}" Value="False"> <Setter Property="Visibility" Value="Collapsed"/> </DataTrigger> </Style.Triggers> </Style> </Window.Resources> <Grid x:Name="editGrid" Margin="5" Style="{StaticResource ProductGridStyle}"> </Grid> <TextBlock Style="{StaticResource UpdateMessageStyle}" Text="Your changes are saved." />
The advantage of this technique is that the View Model does not contain WPF-specific data types. You get better code/markup separation. A designer could even use these triggers to begin animations, and the code would not have to change.
Conclusion
The Update Controls way is quite a bit different from the usual way of implementing the View Model pattern. Update Controls works best when the View Model stores no state, when the user’s selection is moved into a separate Navigation Model, and when DataTriggers control view properties and animations. I owe a dept of gratitude once again to Craig Shoemaker, this time for creating this reference application. By comparing these implementations side-by-side, you can decide whether this style of programming is right for you. If you can get used to the differences, I think you’ll find that the Update Controls code is easier to maintain over the long term.
Recent comments
3 years 13 weeks ago
3 years 19 weeks ago
3 years 44 weeks ago
3 years 46 weeks ago
3 years 47 weeks ago
4 years 3 days ago
4 years 3 days ago
4 years 17 weeks ago
4 years 17 weeks ago
4 years 21 weeks ago