다음을 통해 공유


DM-V-VM part 8: View Models

Ok, time to wrap this up :-)

Now, we'll finally build what I'm calling a view model for managing a portfolio of stocks (building on the StockModel example from previous posts). A view model is a class that will be used as the DataContext for a data template that will be used to provide the UI for the model. Ideally, there is no code behind required for the UI. In practice, some glue code is often required, but in this example, I won't need any.

First, let's come up with the model. Unlike the data model, there is no base class we'll use here. We just need to provide properties, events, etc. that the UI can bind to. In this example, none of our properties will ever change value, so we don't need to implement INotifyPropertyChanged, but often you will need to. Our UI is going to be pretty simple. We'll provide a list of StockModels for the UI to bind to and provide CommandModels to add and remove stocks. Let's start with the constructor where we set up some fields and some accessors:

        public PortfolioViewModel(IStockQuoteProvider quoteProvider)

        {

            _quoteProvider = quoteProvider;

            _stockModels = new ObservableCollection<StockModel>();

            _stockModels.Add(new StockModel("MSFT", _quoteProvider));

 

            _addCommand = new AddCommand(this);

            _removeCommand = new RemoveCommand(this);

        }

 

        public ObservableCollection<StockModel> Stocks

        {

            get { return _stockModels; }

        }

 

        public CommandModel AddCommandModel

        {

            get { return _addCommand; }

        }

 

        public CommandModel RemoveCommandModel

        {

            get { return _removeCommand; }

        }

 

        private ObservableCollection<StockModel> _stockModels;

        private CommandModel _addCommand;

        private CommandModel _removeCommand;

        private IStockQuoteProvider _quoteProvider;

 The quote provider lets us pass in any quote provider and makes it really easy to unit test the class. We populate the stock list with MSFT, but a real implementation may read it from some saved settings or something.

Next, let's define AddCommand. It will take the stock to add as its parameter:

        private class AddCommand : CommandModel

        {

            public AddCommand(PortfolioViewModel viewModel)

            {

                _viewModel = viewModel;

            }

 

            public override void OnQueryEnabled(object sender, CanExecuteRoutedEventArgs e)

            {

                string symbol = e.Parameter as string;

                e.CanExecute = (!string.IsNullOrEmpty(symbol));

                e.Handled = true;

            }

 

            public override void OnExecute(object sender, ExecutedRoutedEventArgs e)

            {

                string symbol = e.Parameter as string;

                _viewModel._stockModels.Add(new StockModel(symbol, _viewModel._quoteProvider));   

            }

 

            private PortfolioViewModel _viewModel;

        }

And, RemoveCommand, which will take a StockModel as its parameter:

        private class RemoveCommand : CommandModel

        {

            public RemoveCommand(PortfolioViewModel viewModel)

            {

                _viewModel = viewModel;

            }

 

            public override void OnQueryEnabled(object sender, CanExecuteRoutedEventArgs e)

            {

                e.CanExecute = e.Parameter is StockModel;

                e.Handled = true;

            }

 

            public override void OnExecute(object sender, ExecutedRoutedEventArgs e)

            {

                _viewModel._stockModels.Remove(e.Parameter as StockModel);

            }

 

            private PortfolioViewModel _viewModel;

        }

 Now, how do we use this? Well, if we define a data template for the PortfolioViewModel, then we can put a PortfolioViewModel in any ContentControl. We could even have a list of them in an ItemsControl! We can put the data template in App.xaml and it can be used anywhere in the application. Here's an ugly, but functional template on top of the view model:

    <DataTemplate DataType="{x:Type local:PortfolioViewModel}">

      <DockPanel LastChildFill="True">

        <TextBlock DockPanel.Dock="Top" TextAlignment="Center">Portfolio View</TextBlock>

        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">

          <TextBox Name="AddSymbol" Width="100" />

          <Button Command="{Binding AddCommandModel.Command}"

                  CommandParameter="{Binding Path=Text,ElementName=AddSymbol}"

                  local:CreateCommandBinding.Command="{Binding AddCommandModel}"

                  >

            Add

            </Button>

          <Button Margin="50,0,0,0"

                  Command="{Binding RemoveCommandModel.Command}"

                  CommandParameter="{Binding Path=SelectedItem,ElementName=StockList}"

                  local:CreateCommandBinding.Command="{Binding RemoveCommandModel}"

                  >

            Remove

          </Button>

        </StackPanel>

        <ListBox Name="StockList" ItemsSource="{Binding Stocks}" />

      </DockPanel>

    </DataTemplate>

 The add button functionality is much like the way we dealt with commands in part 7. The remove button's command parameter is bound to the selected item in the list box - pretty cool, huh? It will be disabled if nothing is selected and use the selected StockModel if one is selected. The ListBox is bound to the list of StockModels. To make each item display correctly, we need a data template for StockModels. Here's a simple implementation:

    <DataTemplate DataType="{x:Type local:StockModel}">

      <StackPanel Orientation="Horizontal" local:ActivateModel.Model="{Binding}">

        <TextBlock Text="{Binding Symbol}" Width="100"/>

        <TextBlock Text="{Binding Quote}" />

      </StackPanel>

    </DataTemplate>

Now, we need to put a Portfolio view into our window. We can do so by using the following XAML:

<Window x:Class="ModelSample.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    Title="ModelSample" Height="300" Width="300"

    >

    <Grid>

      <ContentControl x:Name="_content" />

    </Grid>

</Window>

and, the following code behind:

    public partial class Window1 : Window

    {

 

        public Window1()

        {

            InitializeComponent();

 

            _content.Content = new PortfolioViewModel(new MockQuoteProvider());

        }

 

    }

The end result looks something like:

Ok, it's not very pretty, but the visuals are all in Xaml and a designer can use Expression or the raw HTML to improve the look. Meanwhile, the behavior is all in PortfolioViewModel, which is easily unit testable because it doesn't depend on any UI.

This example is slightly idealized. I was able to use the attached property tricks to completely eliminate the code behind, but that's not always possible. Sometimes you are left with some glue code that may or may not be easily unit testable. And, I've found in practice that deisgners often need behavior tweaks to implement the UI they want. For example, they may need events to trigger animations. But, I hope I've given the general idea in this series of posts.

I'm including a sample project with demonstrates this all. Unfortunately, I never got around to updating the unit tests after adding the activation stuff to the DataModel class, so there are no unit tests. If there is enough interest, I'll add unit tests. I hope it's easy to see how they would be written.

ModelSample.zip

Comments

  • Anonymous
    September 17, 2006
    Great posts Dan! Thanks a lot. I've been waiting for the wrap up since Friday and enjoed it greatly.

    It would be interesting to see how one can retrofit your ideas into 2.0 based apps. Like CAB/SCSF based. Any comments on that?

  • Anonymous
    September 18, 2006
    Unfortunately, I don't know much about CAB/SCSF. Maybe someone else can comment.

  • Anonymous
    September 18, 2006
    The comment has been removed

  • Anonymous
    October 01, 2006
    Thank you Dan for this great series! I like it very much!I actually use some ideas of your concepts for 3D. What im currently looking for is activating / deactivating for DataModels, which are databined to 3D models. It's a little bit complicated, because none of 3D classes expose any events like "Loaded" or "Unloaded" like FrameworkElements do. I saw you also use 3D in Max. What were your tactics with 3D & DM-M-VM pattern?

  • Anonymous
    October 02, 2006
    In Max, the models were displayed in VisualBrushes. We had to set the visual brushes as logical children of the 2D view that hosts the 3D view and when we did that, we got loaded and unloaded events. If you aren't using visual brushes, you could do something like add an interface that your models implement and have the 2D view that hosts the 3D look for models with that interface in its loaded/unloaded handlers.

  • Anonymous
    October 11, 2006
    I thought I should add a post with the full list of posts in the D-V-VM pattern. They are: DataModel-View-ViewModel