Page 1 of 1

[WPF] Exploring MVVM (Model View ViewModel) II

#1 andrewsw  Icon User is offline

  • It's just been revoked!
  • member icon

Reputation: 3614
  • View blog
  • Posts: 12,437
  • Joined: 12-December 12

Posted 28 September 2014 - 06:00 AM

Part 1

The ViewModels

I'll present the ViewModels collectively here, before finishing with the Views. If you prefer, you could create one or two of the ViewModels and then move on to the View(s), so that you could run and test the application earlier.

In the ViewModels folder create ToDoVM.cs:
using System;
using ToDoMVVM.Models;

namespace ToDoMVVM.ViewModels {
    class ToDoVM : ObservableObject {
        private ToDo _todo;
        private int? _daysLeft = null;

        public ToDoVM() {
            _todo = new ToDo();
        }

        public string Title {
            get { return _todo.Title; }
            set {
                if (value != _todo.Title) {
                    _todo.Title = value;
                    onpropertychanged("Title");
                }
            }
        }
        public DateTime? StartDate {
            get { return _todo.StartDate; }
            set {
                if (value != _todo.StartDate) {
                    _todo.StartDate = value;
                    onpropertychanged("StartDate");
                }
            }
        }
        public DateTime? DueDate {
            get { return _todo.DueDate; }
            set {
                if (value != _todo.DueDate) {
                    _todo.DueDate = value;
                    onpropertychanged("DueDate");
                    onpropertychanged("DaysLeft");
                }
            }
        }
        public DateTime? CompletedDate {
            get { return _todo.CompletedDate; }
            set {
                if (value != _todo.CompletedDate) {
                    _todo.CompletedDate = value;
                    onpropertychanged("CompletedDate");
                    onpropertychanged("DaysLeft");
                }
            }
        }
        // notice that Note is not included

        public int? DaysLeft {
            get {
                if (_todo.DueDate != null && _todo.CompletedDate == null) {
                    if (_todo.DueDate > DateTime.Now) {
                        return ((DateTime)_todo.DueDate - DateTime.Now).Days;
                    } else {
                        return null;
                    }
                } else {
                    return null;
                }
            }
        }
    }
}


(I've named this as a ViewModel (VM) but I don't actually use it directly to feed a View, although it could potentially be used in this way.)

This is almost a mirror of ToDo.cs, wrapping an instance of this class. However, notice that the Note property is not exposed by this ViewModel. It also includes a new property of DaysLeft. The addition of the two onpropertychanged("DaysLeft"); statements will cause DaysLeft to be re-evaluated for display in the Window (in the current View).

Rather than repeating the ToDo properties, quite often a ViewModel just returns an object from the Model as a property. Provided the model implements INotifyPropertyChanged, the DataBindings will be notified of changes, and the UI will be updated.

Remember that this is a small, sample, application, and it doesn't use a database. In a larger application the differences between, and usefulness of, the Model and ViewModels would be more apparent.

Here is the most significant class ToDoListVM.cs, particularly in terms of our understanding the MVVM pattern:
using System;
using System.Windows.Input;
using ToDoMVVM.Models;
using cview = System.Windows.Data.CollectionViewSource;

namespace ToDoMVVM.ViewModels {
    class ToDoListVM : MyObservableCollection<ToDoVM> {
        private ICommand _firstCommand;
        private ICommand _previousCommand;
        private ICommand _nextCommand;
        private ICommand _lastCommand;
        private ICommand _addCommand;
        private ICommand _deleteCommand;

        public ICommand FirstCommand {
            get {
                if (_firstCommand == null) {
                    _firstCommand = new RelayCommand(
                        param => FirstToDo(),
                        param => this.CanMoveBack()
                            );
                }
                return _firstCommand;
            }
        }
        public ICommand PreviousCommand {
            get {
                if (_previousCommand == null) {
                    _previousCommand = new RelayCommand(
                        param => PreviousToDo(),
                        param => this.CanMoveBack()
                            );
                }
                return _previousCommand;
            }
        }
        public ICommand NextCommand {
            get {
                if (_nextCommand == null) {
                    _nextCommand = new RelayCommand(
                        param => NextToDo(),
                        param => this.CanMoveForward()
                            );
                }
                return _nextCommand;
            }
        }
        public ICommand LastCommand {
            get {
                if (_lastCommand == null) {
                    _lastCommand = new RelayCommand(
                        param => LastToDo(),
                        param => this.CanMoveForward()
                            );
                }
                return _lastCommand;
            }
        }
        public ICommand AddCommand {
            get {
                if (_addCommand == null) {
                    _addCommand = new RelayCommand(
                        param => AddToDo(),
                        param => true
                            );
                }
                return _addCommand;
            }
        }
        public ICommand DeleteCommand {
            get {
                if (_deleteCommand == null) {
                    _deleteCommand = new RelayCommand(
                        param => DeleteToDo(),
                        param => (this.Count > 1)   // keep at least 1 item
                            );
                }
                return _deleteCommand;
            }
        }

        private void FirstToDo() {
            this.MoveFirst();
        }
        private void PreviousToDo() {
            this.MovePrevious();
        }
        private void NextToDo() {
            this.MoveNext();
        }
        private void LastToDo() {
            this.MoveLast();
        }
        private void AddToDo() {
            this.Add(new ToDoVM());
            this.MoveLast();
        }
        private void DeleteToDo() {
            this.Remove((ToDoVM)cview.GetDefaultView(this).CurrentItem);
        }
    }
}


It inherits from MyObservableCollection<ToDoVM> which will do the navigational work for us.

MyObservableCollection<ToDoVM> manages ToDoVM instances (the previous code), so only those properties that we have made accessible in that class are accessible to the View.

Note that it is possible, and often occurs, that a ViewModel for a collection works directly with the Model (ToDo, rather than ToDoVM). This isn't considered to break the pattern because the View is still operating under the aegis of a ViewModel.

Here is the RelayCommand in action (excusing the pun):
        public ICommand FirstCommand {
            get {
                if (_firstCommand == null) {
                    _firstCommand = new RelayCommand(
                        param => FirstToDo(),
                        param => this.CanMoveBack()
                            );
                }
                return _firstCommand;
            }
        }


The Command resolves to a call to FirstToDo(), and the boolean value of this.CanMoveBack() determines whether this method can execute. That is, FirstToDo() won't be called if CanMoveBack() is false.
        private void FirstToDo() {
            this.MoveFirst();
        }


This is just a wrapper-call, and we could instead write param => this.MoveFirst(),. Doing this will not cause issues associated with closures that you may have encountered with, for example, Javascript. this means this in .NET. (I expect someone will now add a comment below, showing that this doesn't always mean this!) Personally, I prefer to use the wrapper-calls, it makes the code easier to read, and to maintain in the longer term, in my opinion.

Because we created MyObservableCollection, here is where we can introduce our own logic, or business logic (in addition to the can execute value):
        private void AddToDo() {
            this.Add(new ToDoVM());
            this.MoveLast();
        }
        private void DeleteToDo() {
            this.Remove((ToDoVM)cview.GetDefaultView(this).CurrentItem);
        }


ToDoTableVM.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ToDoMVVM.ViewModels {
    class ToDoTableVM {
        private ToDoListVM _toDoList;

        public ToDoTableVM(ToDoListVM ExistingList) {
            ToDoList = ExistingList;
        }
        public ToDoListVM ToDoList {
            get {
                return _toDoList;
            }
            set {
                if (value != null)
                    _toDoList = value;
            }
        }
    }
}


This is a simple wrapper-class for a ToDoListVM, which will be used with the ListView version of the ToDo list. This is necessary because of our use of DataTemplates, discussed in a while. Basically, using DataTemplates as Views means that we need a different ViewModel for each DataTemplate.

Notice that ToDoList is exposed as a property of this class, rather than just as a public field. This is necessary to continue to work with the ObservableCollection. ObservableCollections work with properties, not fields.

Here is the simplest of files, HomeMV.cs, and final ViewModel:
using System;

namespace ToDoMVVM.ViewModels {
    class HomeVM {
    }
}


Again, this is necessitated by our use of DataTemplates. The Home View is just a simple welcome message, but we still need a ViewModel for it.

The Views

Here is the completed code of Mainwindow.xaml:
<Window x:Class="ToDoMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ToDoMVVM" 
        xmlns:viewmodels="clr-namespace:ToDoMVVM.ViewModels"
        Title="Main Window" Style="{StaticResource styWindow}" SizeToContent="WidthAndHeight"
        MinWidth="500" MinHeight="350">
    <window.CommandBindings>
        <CommandBinding Command="ApplicationCommands.Close" Executed="CloseCommandHandler" />
    </window.CommandBindings>
    <window.Resources>
        <DataTemplate DataType="{x:Type viewmodels:HomeVM}">
            <TextBlock Text="Welcome to the ToDo Application!" FontSize="20" 
                       HorizontalAlignment="Center" VerticalAlignment="Center" />
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:ToDoListVM}">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition />
                    <ColumnDefinition />
                    <ColumnDefinition />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <Label Grid.Row="0" Grid.Column="0" Content="Title" Margin="5" />
                <TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3" 
                         HorizontalAlignment="Center" VerticalAlignment="Center" 
                         MinWidth="200"
                         Text="{Binding /Title}" Padding="10,5,10,5" Margin="5" />
                <Label Grid.Row="1" Grid.Column="0" Content="Start Date" />
                <DatePicker Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2">
                    <DatePicker.SelectedDate>
                        <Binding Path="/StartDate" Mode="TwoWay" />
                    </DatePicker.SelectedDate>
                </DatePicker>
                <Label Grid.Row="2" Grid.Column="0" Content="Due Date" />
                <DatePicker Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2">
                    <DatePicker.SelectedDate>
                        <Binding Path="/DueDate" Mode="TwoWay" />
                    </DatePicker.SelectedDate>
                </DatePicker>
                <Label Grid.Row="2" Grid.Column="3" Content="{Binding Path=/DaysLeft}" />
                <Label Grid.Row="3" Grid.Column="0" Content="Completed Date" />
                <DatePicker Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2">
                    <DatePicker.SelectedDate>
                        <Binding Path="/CompletedDate" Mode="TwoWay" />
                    </DatePicker.SelectedDate>
                </DatePicker>
                <StackPanel Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="4" MaxHeight="40"
                            Orientation="Horizontal">
                    <Button Content="First" Command="{Binding Path=FirstCommand}" />
                    <Button Content="Previous" Command="{Binding Path=PreviousCommand}" />
                    <Button Content="Next" Command="{Binding Path=NextCommand}" />
                    <Button Content="Last" Command="{Binding Path=LastCommand}" />
                    <Button Content="Add" Command="{Binding Path=AddCommand}" />
                    <Button Content="Delete" Command="{Binding Path=DeleteCommand}" />
                </StackPanel>
            </Grid>
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:ToDoTableVM}">
            <ListView x:Name="TheList" ItemsSource="{Binding Path=ToDoList}" 
                      IsSynchronizedWithCurrentItem="True">
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="Title" Width="200" 
                                        DisplayMemberBinding="{Binding Path=Title}" />
                        <GridViewColumn Header="Start Date" Width="120" 
                                        DisplayMemberBinding="{Binding Path=StartDate}" />
                        <GridViewColumn Header="Due Date" Width="120" 
                                        DisplayMemberBinding="{Binding Path=DueDate}" />
                        <GridViewColumn Header="Completed Date" Width="120" 
                                        DisplayMemberBinding="{Binding Path=CompletedDate}" />
                    </GridView>
                </ListView.View>
            </ListView>
        </DataTemplate>
    </window.Resources>
    <DockPanel>
        <Border DockPanel.Dock="Left" BorderBrush="Blue" BorderThickness="0,0,1,0">
            <StackPanel DockPanel.Dock="Left">
                <Button Content="ToDo Item" Name="btnToDoItem" Click="btnToDoItem_Click"/>
                <Button Content="All Items" Name="btnAllItems" Click="btnAllItems_Click" />
                <Button Content="Home" Name="btnHome" Click="btnHome_Click" />
                <Button Content="Exit" Name="btnExit" Command="ApplicationCommands.Close" />
        </StackPanel>
        </Border>
        <ContentControl Content="{Binding}" />
    </DockPanel>
</Window>


    <window.Resources>
        <DataTemplate DataType="{x:Type viewmodels:HomeVM}">
            <TextBlock Text="Welcome to the ToDo Application!" FontSize="20" 
                       HorizontalAlignment="Center" VerticalAlignment="Center" />
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:ToDoListVM}">
            <Grid>


There are three DataTemplates, having moved the code for the TextBlock, Grid and ListView, as mentioned earlier. These DataTemplates are our Views.

Each DataTemplate has a DataType which specifies the ViewModel that each is associated with. When we set the DataContext of the Window to a particular ViewModel, the ContentControl (at the bottom of this file) will be loaded from the DataTemplate that corresponds to the DataContext (the ViewModel).

Another approach is to use User Controls as Views. These User Controls would be stored in the Views folder and code would be used to swap one User Control for another. Two advantages of using DataTemplates are:

  • We only have to set the DataContext of the Window and the appropriate template will automatically be loaded in the ContentControl.
  • We don't have to be concerned about which template is currently in use (visible).

Having just made this second point, if we set the same DataContext a second time the template is actually reloaded. You will see this when you click the same button in the left panel twice in succession: the cursor position or selection will change. So I would, in fact, write a little bit of code to check that the DataContext we are attempting to change to isn't the same as the one currently in use.

We have already encountered the main issue with DataTemplates, that they each require their own ViewModel. This would concern me with a larger application, because it would be useful to be able to re-use the same ViewModel for more than one View. It is quite common to have Views that only differ in the way that they present essentially the same data.

Another issue is that Mainwindow.xaml could get very large. (I'll guess that it may be possible to move DataTemplates to separate files, but I haven't investigated this possibility.)

Code Behind

Here is the final code for Mainwindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using ToDoMVVM.ViewModels;

namespace ToDoMVVM {
    /// <summary>
    /// Interaction logic for Mainwindow.xaml
    /// </summary>
    public partial class MainWindow : Window {
        private HomeVM _home;
        private ToDoListVM _list;
        private ToDoTableVM _listT;

        public MainWindow() {
            InitializeComponent();
            Loaded += MainWindow_Loaded;
        }

        void MainWindow_Loaded(object sender, RoutedEventArgs e) {
            _list = new ToDoListVM();
            _list.Add(new ToDoVM() { Title = "Your Title Here" });
            _home = new HomeVM();
            this.DataContext = _home;
        }

        private void btnAllItems_Click(object sender, RoutedEventArgs e) {
            if (_listT == null) {
                _listT = new ToDoTableVM(_list);
            }
            this.DataContext = _listT;
        }

        private void btnToDoItem_Click(object sender, RoutedEventArgs e) {
            this.DataContext = _list;
        }

        private void btnHome_Click(object sender, RoutedEventArgs e) {
            this.DataContext = _home;
        }

        private void CloseCommandHandler(object sender, ExecutedRoutedEventArgs e) {
            this.Close();
        }
    }
}


    public partial class MainWindow : Window {
        private HomeVM _home;
        private ToDoListVM _list;
        private ToDoTableVM _listT;


Rather than instantiating ViewModels each time a button on the left is clicked, I keep a reference to each one, and only instantiate each once when the corresponding template/View is first loaded. If I weren't using DataTemplates then _listT (and its ViewModel) would be redundant. _home is unnecessary anyway: it's the simplest of classes and there is no harm in creating it anew each time.

For a larger application you would need to consider whether retaining all these object references was a good idea.
        void MainWindow_Loaded(object sender, RoutedEventArgs e) {
            _list = new ToDoListVM();
            _list.Add(new ToDoVM() { Title = "Your Title Here" });
            _home = new HomeVM();
            this.DataContext = _home;
        }


Here's the kick-off point! This creates a new ToDo list and populates it with a dummy item. The DataContext is defaulted to the Home-ViewModel, which just displays a simple welcome-message.
        private void btnAllItems_Click(object sender, RoutedEventArgs e) {
            if (_listT == null) {
                _listT = new ToDoTableVM(_list);
            }
            this.DataContext = _listT;
        }


This changes the DataContext (causing a different DataTemplate to be loaded), but only creates the ViewModel once.

Notice that I am using simple event-handlers (code-behind) for these buttons. Commands could be used instead, perhaps also creating an ApplicationViewModel, as suggested here, to store and maintain the state of the application as a whole. I decided to use event-code as I thought it would be useful for you to see all of this central/controlling code in one place.



When you run and test the application you'll notice that the DaysLeft just appears as a number, e.g. 4 rather than "4 Days". Also, the dates will appear in the ListView in full: 9/28/2014 12:00:00 AM. These formats can be changed using ValueConverters. I would probably store these converters in the Views folder, as they are presentational aspects and may only be relevant to WPF.

An alternative is to create properties in the ViewModel that create these string values, and bind to these new properties. There is nothing wrong with this, and it demonstrates another useful aspect of ViewModels. ValueConverters are the WPF-way to do this, and quite a cool feature.

The ListView is quite plain and read-only. It is highly customizable though (as are all WPF controls/elements), particularly with the aide of ItemTemplates. The rows could be re-designed to include TextBoxes and DatePickers, making the data read/write. Eventually, we could make the ListView behave as a DataGrid (a DataGridView in WinForms). Alternatively, of course, we could use a DataGrid, which is a fairly recent addition to the toolbox.

This post has been edited by andrewsw: 28 September 2014 - 03:48 PM


Is This A Good Question/Topic? 0
  • +

Replies To: [WPF] Exploring MVVM (Model View ViewModel) II

#2 andrewsw  Icon User is offline

  • It's just been revoked!
  • member icon

Reputation: 3614
  • View blog
  • Posts: 12,437
  • Joined: 12-December 12

Posted 08 October 2014 - 11:38 AM

Attached is an updated version of the application. It includes code to convert "20" to "20 Days" (for the task to be completed) and uses a DateConverter to display shortened versions of the dates in the ListView.

It also includes Load and Save buttons for the list of ToDo tasks. It executes these commands from the code-behind but it should be considered to remove all this code-behind, creating an ApplicationViewModel (as mentioned above) and binding to the Commands in the XAML.

Attached File  ToDoMVVM.zip (15.99K)
Number of downloads: 7

Attached Image
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1