다음을 통해 공유


Developing Reusable Controls with the Model-View-ViewModel Pattern

There have been several great blogs and articles about the Model-View-ViewModel pattern lately. I thought I would share my own example and thoughts on it – specifically on using this pattern to develop reusable controls.

Rather than re-explain what the Model-View-ViewModel pattern is I suggest you read these other articles. I recommend

· https://msdn.microsoft.com/en-us/magazine/dd419663.aspx

· https://blogs.msdn.com/johngossman/

· https://blogs.msdn.com/dphill/archive/2009/01/31/the-viewmodel-pattern.aspx

When developing a reusable control often you know little, if anything, about the Model. That is the owned completely by the application using the control. So the control consists of the View and the ViewModel, which provide an interface for the application to attach the Model. Exactly how the interface looks depends on the requirements of the control.

The Example

I like examples more than pages of text, so I’m going to present an example of a reusable control implemented using the M-V-VM pattern. I wanted an example which was complex enough to show the advantages of the pattern, but not something made up. I decided to do a Column Picker control.

When I first came up with this example and demo’ed the code to some people they remarked that it wouldn’t work with Silverlight because it used RoutedCommands and the ControlTemplate used bindings to other template elements. Silverlight doesn’t support those features. So I reworked the example into something which is more Silverlight friendly. The following explanation uses the Silverlight friendly version. At the end I’ll explain how the WPF version was different.

The ColumnPicker allows the user to manage a set of columns for a data grid type control. There are 2 column sets – the visible columns and the hidden ones. The visible columns are the ones the user can see in the data grid. The visible columns can also be re-ordered.

The ColumnManager

It is easiest to start by considering the requirements of the view model. The ViewModel should expect to be a data binding source, so it should expose change notification. INotifyPropertyChanged and INotifyCollectionChanged tend to work well. Specific requirements for the ColumnPicker view model are:

· Expose the set of hidden columns

· Expose the set of visible columns

· Expose the ability to make a column visible

· Expose the ability to make a column hidden

· Expose the ability to change the order of the visible columns

· Hold data about a given column, including

o Display Name

o Identifier

o Property name (on the data object to get the data for a given row for that column)

From these requirements there are obviously 2 different classes. One we’ll call ColumnData which will hold information about a given column. The other we’ll call ColumnManager which will do the “global” column stuff.

Translating the requirements to properties/methods on these objects we can the following interfaces.

    public class ColumnData

    {

       public ColumnData(string id, string name, string property);

 

        public string Id

        {

            get;

        }

        public string Name

        {

            get;

        }

        public string PropertyName

        {

            get;

        }

    }

    public class ColumnManager

    {

        public ColumnManager();

        public ICollection<ColumnData> VisibleColumns

        {

            get;

        }

        public ICollection<ColumnData> HiddenColumns

        {

            get;

        }

        public void AddColumn(ColumnData column);

       

        public void MoveColumnDown(ColumnData column);

        public void MoveColumnUp(ColumnData column);

        public bool CanMoveColumnDown(ColumnData column);

        public bool CanMoveColumnUp(ColumnData column);

        public void ShowColumn(ColumnData column);

        public void HideColumn(ColumnData column);

        public bool CanShowColumn(ColumnData column);

        public bool CanHideColumn(ColumnData column);

    }

ColumnData is immutable, so we don’t have to worry about change notification for those properties.

ColumnManager returns uses ReadOnlyObservableCollection instances for backing field for the VisibleColumns/HiddenColumns. This does 2 things – it provides change notification to anything data bound to it, and it forces all changes to go through the ColumnManager. The property signature is to return ICollection<ColumnData> - this is something I do to encapsulate the concrete type of collection being used. Unfortunately this means the signature doesn’t communicate that the collection supports INotifyCollectionChanged. Perhaps I’ll write another article on this later – for now just humor me that I like doing it that way.

At this point I imagine that you can visualize the implementation of these 2 classes. Neither is very complex, and both should be easily unit tested. One can also visualize how the application might instantiate these objects with the right data, say either hard coded or using heuristics based on the data objects for the data grid, or retrieving the settings from some configuration store.

In this ViewModel example the ViewModel layer actually consists of 2 inner layers. The “model layer” and the “adapter layer.” The “model layer” is the same as the Model only instead of being the model for the system, it is the model for the UI. The “adapter layer” provides an interface which is easily consumed by the View.

For some controls the ViewModel is simple enough that it is only 1 layer, or for the “adapter” part to be very thin and simple. In this example I feel it is complex enough to warrant 2 distinct layers in the ViewModel.

ColumnData and ColumnManager are the “model layer” part. Next we’ll go over the “adapter layer” part.

The ColumnPicker ViewModel

For the adapter part we’ll create a class called ColumnPickerViewModel, a descriptive – if unimaginative – name. Lets consider its requirements. As I’m sure you’ve guessed it main responsibility is to expose the “model layer” to the View in an easy to consume way. So what does the View need? Think about the UI – it has 2 ListBoxes and 4 Buttons. Commands are a great way to hook a ViewModel up to a View. They call Execute when the button is clicked, and update their enabled state based on CanExecute. So 4 commands, 1 per button, is needed. Since the CanExecute value changes based on the selected item in each ListBox we need a get/set property to be double bound to the ListBox.SelectedItem.

If you’ve used commands in WPF before you might be thinking this is simple – just define 4 RoutedCommands, and use element bindings to get the SelectedItem from the right list box as the command parameter. And you’d be totally right that it would work fine. However one of the requirements is for this control and ViewModel to be Silverlight friendly – which I define as being able to port the code to Silverlight with minimal changes.

Silverlight doesn’t have RoutedCommand, or even ICommand. And it doesn’t support bindings to a different element in the template. The element binding issue is easy to work around by putting a property in the ViewModel. Fortunately there are also lots of blogs which demonstrate how to add ICommand support to Silverlight via an Attached Service.

So what the ColumnPickerViewModel needs to have in it is:

· A get/set property for the currently selected hidden column

· A get/set property for the currently selected visible column

· An ICommand object which makes the selected hidden column visible

· An ICommand object which makes the selected visible column hidden

· An ICommand object which moves the selected visible column move up in the ordering

· An ICommand object which moves the selected visible column mode down in the ordering

This gives us an interface of:

    class ColumnPickerViewModel : INotifyPropertyChanged

    {

        public ColumnPickerViewModel(ColumnManager columnManager);

        public event PropertyChangedEventHandler PropertyChanged;

        public ColumnManager Manager

        {

            get;

        }

        public ICommand MoveColumnUpCommand

        {

            get;

        }

        public ICommand MoveColumnDownCommand

        {

            get;

        }

        public ICommand ShowColumnCommand

        {

            get;

        }

        public ICommand HideColumnCommand

        {

            get;

        }

        public ColumnData SelectedHiddenColumn

        {

            get;

            set;

     }

        public ColumnData SelectedVisibleColumn

        {

            get;

            set;

        }

    }

Now the SelectedHiddenColumn/SelectedVisibleColumn properties are straightforward to implement. However there is 1 tricky thing which is if a column moves from hidden to visible and is the SelectedHiddenColumn we need to update the property, since the column is no longer in the hidden set. This is solved by monitoring the VisibleColumns/HiddenColumns collections for changes and verifing that the selected items are still in the set.

The command properties are trivial – assuming we have a nice ICommand object to use. As was stated previously using RoutedCommand doesn’t meet our requirements. So we need to create our own ICommand objects.

The Commands

Lets first consider the ShowColumnCommand and HideColumnCommand. It is pretty trivial to pass in the ColumnPickerViewModel when they are instanciated. The objects then would have everything they need to implement CanExecute/Execute by calling the right ColumnManager method passing in the right Selected*Column value. The only tricky thing is knowing when to raise the CanExecuteChanged event. All we have to do is to subscribe to the property changed event for the right Selected*Column property. When that property changes we raise CanExecuteChanged, which then causes any client to call back via CanExecute. In this model the parameter to CanExecute/Execute is unused.

The MoveColumnUpCommand/MoveColumnDownCommand are simliar in their implementation. The biggest difference is that CanExecute depends not only on the SelectedVisibleColumn, but on its position in the VisibleColumns collection. So we have to monitor the VisibleCollection for changes too.

ShowColumnCommand/HideColumnCommand didn’t cross my threshold for factoring out the common code, but that would certainly be a reasonable thing to do.

Now in my implemenation of these 2 commands I noticed that the differences between the 2 classes was very minimal – just which 2 methods in the ColumnManager were being called. So I refactored the classes into 1, where the differences are encapsolated in a small Strategy object.

And that is the complete “adapter layer” part of the ViewModel. Again we find that the classes are relatively simple. In fact most of the complexity is in monitoring for changes. They are a little more difficult to unit test, because of their dependency on ColumnManager. But fortunatly there are patterns and technology to help deal with that.

Now I’m sure there are some people who are saying that this seems like a lot of work, and wouldn’t it be better to merge of this stuff together, say in ColumnManager. We could discuss this in terms of things like coupling and cohesion, code smells and design principles. But to me it all boils down to the fact that very rarely have I regretted decomposing a problem into smaller classes. And when I don’t decompose it seems more often or not I regrett not doing it. (I also happen to believe that a more formal analysis will also indicate this the right decision.)

Synchronizing ColumnManager and ListView

There is one last part of the ViewModel we need to cover – and that is how to actually use the columns in a list view. For that I’ve created a class called GridViewColumnManager. It takes a ColumnManager and a GridView (from inside a ListView). Its responsibility is to update the GridView’s columns when ColumnManager changes and vice versa.

To do this it subscribes to collection changed event on ColumnManager’s VisibleChildren. When items are added/removed/moved/etc in the collection, it will make a corrisponding change in the Columns collection on the GridView. One interesting thing here is that for Add operations we need to create a GridViewColumn from a ColumnData, a relatively straightforward operation.

The other thing we need to worry about is the user drag/drop columns in the ListView to reorder them. To support that we need to go the other way and update ColumnManager when the GridView columns change ordering.

As you can guess this presents an oportunity for a cycle which never ends. So we have flags to turn off one behavior when in the middle of the other.

The View

Now that we have our ViewModel all done we can move on to the View. I’ve created a ColumnPicker class which inherits from Control. It is more of a placeholder to give us a place to attach XAML and the ViewModel to. It’s implementation is trivial and consists of a property which holds the ViewModel.

The real meat of the View is in the XAML. Here is the template

<ControlTemplate TargetType="{x:Type local:ColumnPicker}">

    <Border Background="{TemplateBinding Background}"

            BorderBrush="{TemplateBinding BorderBrush}"

            BorderThickness="{TemplateBinding BorderThickness}">

        <Grid>

            <Grid.ColumnDefinitions>

                <ColumnDefinition />

                <ColumnDefinition Width="Auto"/>

                <ColumnDefinition />

                <ColumnDefinition Width="Auto"/>

            </Grid.ColumnDefinitions>

            <Grid.RowDefinitions>

                <RowDefinition Height="Auto" />

                <RowDefinition Height="Auto"/>

                <RowDefinition />

            </Grid.RowDefinitions>

            <ListBox x:Name="notShownColumns"

                     Grid.Column="0"

                     Grid.Row="0"

                     Grid.RowSpan="3"

                     ItemsSource="{Binding ViewModel.Manager.HiddenColumns, RelativeSource={RelativeSource TemplatedParent}}"

                     ItemTemplate="{StaticResource ColumnData_Template}"

                     SelectedItem="{Binding ViewModel.SelectedHiddenColumn, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"

      Margin="2" />

            <ListBox x:Name="shownColumns"

                     Grid.Column="2"

                     Grid.Row="0"

                     Grid.RowSpan="3"

                     ItemTemplate="{StaticResource ColumnData_Template}"

  ItemsSource="{Binding ViewModel.Manager.VisibleColumns, RelativeSource={RelativeSource TemplatedParent}}"

                     SelectedItem="{Binding ViewModel.SelectedVisibleColumn, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"

                     Margin="2" />

            <Button x:Name="showColumn"

                    Grid.Column="1"

     Grid.Row="0"

                    Margin="2"

              Command="{Binding ViewModel.ShowColumnCommand, RelativeSource={RelativeSource TemplatedParent}}"

                    HorizontalAlignment="Center"

                    VerticalAlignment="Center">

                Show

            </Button>

            <Button x:Name="unshowColumn"

                    Grid.Column="1"

                    Grid.Row="1"

                    Margin="2"

                    Command="{Binding ViewModel.HideColumnCommand, RelativeSource={RelativeSource TemplatedParent}}"

                    HorizontalAlignment="Center"

                    VerticalAlignment="Center">

                Unshow

            </Button>

            <Button x:Name="moveColumnUp"

                    Grid.Column="3"

                    Grid.Row="0"

                    Margin="2"

                    Command="{Binding ViewModel.MoveColumnUpCommand, RelativeSource={RelativeSource TemplatedParent}}"

                    HorizontalAlignment="Center"

          VerticalAlignment="Center">

                Up

            </Button>

            <Button x:Name="moveColumnDown"

                    Grid.Column="3"

                    Grid.Row="1"

                    Margin="2"

                    Command="{Binding ViewModel.MoveColumnDownCommand, RelativeSource={RelativeSource TemplatedParent}}"

                    HorizontalAlignment="Center"

                    VerticalAlignment="Center">

                Down

            </Button>

        </Grid>

    </Border>

</ControlTemplate>

Lets start w/ the ListBoxes. Not much interesting there. We bind the ItemsSource to the right properties in the ViewModel. We two way bind the SelectedItem property.

The buttons all bind to the commands from the ViewModel. The button’s enabled state is synced w/ CanExecute and when they click the ViewModel updates.

It isn’t hard to imagine being able to drive a very different rendering from those same set of properties. This is because the ViewModel has very few assumptions of what type of visual elements are in the View.

WPF Oriented Design

The original WPF oriented design was slightly different. The main differences were:

· RoutedCommands were used instead of the custom ICommand implementations. The ColumnPickerControl registered handlers for the commands, which then called the right methods in ColumnManager. The parameter to the commands was the selected column.

· The Selected* properties on the ColumnPickerViewModel didn’t exist. Instead the XAML could directly bind the CommandParameter property on the buttons to the SelectedItem property in the ListBoxes.

· ColumnPickerViewModel class didn’t exist, since it doesn’t have any content w/ the Control owning the RoutedCommands and the ElementName binding not requiring VM properties to provide communication between template elements.

Conclusion

Now we are at the end. The entire code is attached for your enjoyment.

I have shown one way to design a column picker control, separating the logic from the actual presentation of the control. I’ve also shown a way to attach that logic and data to a different interface (the GridView columns) and keep them in sync.

All the code is covered by the MS-PL license. It is intended to illustrate concepts and not be production level quality code, so use at your own risk.

 

ModelViewViewModelDemo.zip

Comments