All posts tagged 'mart-client'
 
Technorati Tags: ,,,,

Windows Presentation Foundation provides a lot of "Out Of The Box" features that greatly enhance the development and user experience.  One of these is the auto-hookup of the INotifyPropertyChanged.  What does this mean?  It means that if the data that you bind to in you WPF changes (and correctly implements INotifyPropertyChanged), it will be reflected in the user interface auto-magically!

To demonstrate this, we will create a very simple Product class.

public class Product
{
    public int ID { get; internal set; }
    public string ModelName { get; set; }
    public string SKU { get; set; }
    public decimal Price { get; set; }
    public int Inventory { get; set; }
}

 

Since we don't want to bind directly to the model classes, we will create a (again, very simple) View Model to bind our window to.

public class ChangeNotificationViewModel
{
    public ChangeNotificationViewModel()
    {
        //TODO: Get From Service or Datastore
        Products = new ProductService().GetProducts();
    }
    public ChangeNotificationViewModel(IList<Product> products)
    {
        _products = products;
    }
    private IList<Product> _products;
    public IList<Product> Products
    {
        get { return _products; }
        set { _products = value; }
    }
    private ICommand _updatePrice;
    public ICommand UpdatePriceCommand
    {
        get
        {
            if (_updatePrice == null)
            {
                _updatePrice = new PriceUpdater();
            }
            return _updatePrice;
        }
        set { _updatePrice = value; }
    }
    private class PriceUpdater :ICommand
    {
        public void Execute(object parameter)
        {
            var p = parameter as Product;
            if (p == null) return;
            p.Price *= 1.1M;
        }
        public bool CanExecute(object parameter)
        {
            return true;
        }
        public event EventHandler CanExecuteChanged;
    }
}

 

A couple things to point out about the view model. 

 


  • We use Dependency Injection in one constructor to take an IList<Product>.  This allows us to better test our view model, and opens up the door for several different methods of getting the constituted data into the View Model.
  • The internal constructor is in place to get some initial data as we are spiking up the Window so we reduce complexity.
  • The only real code that the WPF Window cares about is the Products and the UpdatePriceCommand properties, as that is what the Window will bind to.
  • This is a very rudimentary implementation of the Command Pattern.  I will enhance this in a future blog post as the application gets flushed out into a true MVVM implementation.  Typically firing off a UI specific item like a MessageBox is not a good idea in the View Model, but it is here to demonstrate the INotifyPropertyChanged concept that this post is focusing on.

 

Now let's get a very simple WPF Window coded up to display the data. The ComboBox allows for the selection of the product, and once one is selected, the text boxes are populated with the values for the product. The Window that we create looks like this:

 

Notify1

 

And the XAML for the window looks like this:

 

 

<Window x:Class="WPF.Views.ChangeNotification"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Change Notification" Height="200" Width="300">
    <Grid Grid.IsSharedSizeScope="True">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0" Grid.Column="0" HorizontalAlignment="Stretch" Height="Auto">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Label"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Grid.Row="0" Grid.Column="0" Margin="0,0,1,0" HorizontalAlignment="Right">Products:</Label>
            <ComboBox Name="ProductSelector" Grid.Row="0" Grid.Column="1" Margin="2,1,2,1"
                      ItemsSource="{Binding Path=Products}"
                      DisplayMemberPath="ModelName"/>
        </Grid>
        <Grid Grid.Row="1" Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Label"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Grid.Row="0" Grid.Column="0" Margin="0,0,1,0" HorizontalAlignment="Right">ModelName:</Label>
            <TextBox Grid.Row="0" Grid.Column="1" Margin="1,1,1,1" HorizontalAlignment="Stretch"
                     Text="{Binding ElementName=ProductSelector, Path=SelectedItem.ModelName}"></TextBox> 
            <Label Grid.Row="1" Grid.Column="0" Margin="0,0,1,0" HorizontalAlignment="Right">SKU:</Label>
            <TextBox Grid.Row="1" Grid.Column="1" Margin="1,1,1,1" HorizontalAlignment="Stretch"
                     Text="{Binding ElementName=ProductSelector, Path=SelectedItem.SKU}"></TextBox>
            <Label Grid.Row="2" Grid.Column="0" Margin="0,0,1,0" HorizontalAlignment="Right">Price:</Label>
            <TextBox Grid.Row="2" Grid.Column="1" Margin="1,1,1,1" HorizontalAlignment="Stretch"
                     Text="{Binding ElementName=ProductSelector, Path=SelectedItem.Price}"></TextBox>
            <Label Grid.Row="3" Grid.Column="0" Margin="0,0,1,0" HorizontalAlignment="Right">Inventory:</Label>
            <TextBox Grid.Row="3" Grid.Column="1" Margin="1,1,1,1" HorizontalAlignment="Stretch"
                     Text="{Binding ElementName=ProductSelector, Path=SelectedItem.Inventory}"></TextBox>
        </Grid>
        <StackPanel Grid.Row="2" Grid.Column="0">
            <Button Content="Update Price" 
                    Command="{Binding Path=UpdatePriceCommand}" 
                    CommandParameter="{Binding ElementName=ProductSelector, Path=SelectedItem}">
            </Button>
        </StackPanel>
    </Grid>
</Window>

 

Clicking on the Button increases the price of the currently selected product by 10%, then shows the new price in a message box.  However, the data bound text box doesn't change the Price text to reflect the updated price.

 

This is where in WinForms we would need to add event handlers or other types of call backs to make sure the UI accurately reflects the state of the data when a change occurs.  In WPF, however, we merely need to correctly implement INotifyPropertyChanged and it all gets wired up automagically!

 

To implement the pattern, we need to change all properties that we care about from auto properties to properties with backing fields.  This is so we can raise the NotifyPropertyChanged event in the setter.  The INotifyPropertyChanged interface consists of one event (PropertyChanged). The event arguments (PropertyChangedEventArgs) has one property that gets set in the constructor, and that is the property name that changed.  WPF will then poll its bindings to look for anything bound to that property and requery the data context for the current value.

public event PropertyChangedEventHandler PropertyChanged;
private void onPropertyChanged(string fieldName)
{
   if (PropertyChanged != null)
   {
       PropertyChanged(this,new PropertyChangedEventArgs(fieldName));
   }
}

 

The first issue I have with this implementation is that we need to pass in a string for the property name, which can lead to run time errors if the properties change in our entity but the "magic strings" don't get updated in our code.  If you have good test coverage, then this will get caught in development, but additionally, I create an enumeration of all of the properties on the entity, and pass the enumerated value into event raiser. The enumeration get updated each time I regenerate my entities from the data base (I use CodeSmith templates), so I get compile time checking for my property names.  Here is the code for the Product class refactored:

public class Product : INotifyPropertyChanged
{
    public enum FieldNames
    {
        ID,
        ModelName,
        SKU,
        Price,
        Inventory
    }
    private int _id;
    public int ID
    {
        get { return _id; }
        set
        {
            if (_id == value) return;
            _id = value;
            onPropertyChanged(FieldNames.ID);
        }
    }
    private string _modelName;
    public string ModelName
    {
        get { return _modelName; }
        set
        {
            if (_modelName == value) return;
            _modelName = value;
            onPropertyChanged(FieldNames.ModelName);
        }
    }
    private string _sku;
    public string SKU
    {
        get { return _sku; }
        set
        {
            if (_sku == value) return;
            _sku = value;
            onPropertyChanged(FieldNames.SKU);
        }
    }
    private decimal _price;
    public decimal Price
    {
        get { return _price; }
        set
        {
            if (_price == value) return;
            _price = value;
            onPropertyChanged(FieldNames.Price);
        }
    }
    private int _inventory;
    public int Inventory
    {
        get { return _inventory; }
        set
        {
            if (_inventory == value) return;
            _inventory = value;
            onPropertyChanged(FieldNames.Inventory);
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    private void onPropertyChanged(Enum fieldName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this,new PropertyChangedEventArgs(fieldName.ToString()));
        }
    }
}

 

There are a couple of items missing from this implementation that will be discussed in a future post:

 


  • IsNew: This is a calculated field that indicates if the entity is transient (has not yet been persisted)
  • IsDirty: This is also a calculated field that indicate if the entity has been updated (the onPropertyChanged method sets the property)
  • INotifyCollectionChanged: This interface allows for WPF to update elements bound to collections when the collection changes

 

In my next post I get rid of the MessageBox and show MultiBindings for CommandParameters.

 

Happy Coding!


.NET Musings

Wandering thoughts of a developer, architect, speaker, and trainer

Managed Windows Shared Hosting by OrcsWeb