Page 1 of 1

WPF Build An Application 4

#1 andrewsw  Icon User is offline

  • It's just been revoked!
  • member icon

Reputation: 3623
  • View blog
  • Posts: 12,510
  • Joined: 12-December 12

Posted 26 May 2014 - 01:23 PM

My goal with this part is to end with a working Application. To me this means being able to save and load a list of ToDo items.

Part 3

In order to achieve this I have removed the isValid method and deferred any further discussion of Data Validation. This is a detailed subject and requires separate study. I have, however, kept the validation, and error-display, for the Title, and added a simple piece of validation so that the current ToDo list cannot be saved if there is an empty Title.

I also display Item x of y in the StatusBar and add a coloured-border around the ComboBox for a High or Low priority, which uses a DataTemplate and DataTemplate.Trigger.

Topic List:

  • Generic Commands and Other Changes
  • Display Item x Of y, using Value Converter
  • DataTemplates and DataTemplate.Triggers
  • Serializing Objects
  • Commands to Load and Save (and Exit) using..
  • Open and SaveFile Dialogs
  • Brief Introduction to MVVM
  • Next Steps


Attached Image

Generic Commands and Other Changes

There are a number of changes to be made to the current version of the application. Firstly, we need to delete our CurrentPosition and GetDefaultView methods in MyObservableCollection.cs. These are masking properties of the same names that are already available to use from the DataContext, and will prevent us from displaying 'Item x Of y' in the StatusBar. I also include a using-alias of cview.

MyObservableCollection.cs (full code)

Spoiler

We now need to modify NavigationCommand.cs to acknowledge these changes. I also take the opportunity to make these commands more generic: notice that there is no metion of ToDo in the code. To achieve this also requires the addition of a new constraint:
namespace ToDoApplication.Models {
    class NavigationCommand<T> : ICommand where T : new() {
        private readonly MyObservableCollection<T> _list;


This requires that the type T must have a public parameterless constructor, so that we can use _list.Add(new T()) in the Command to create a new instance of the type.

NavigationCommand.cs (full code)

Spoiler

This also no longer uses the isValid method from ToDo.cs, so remove this method from that file - this one:
        internal bool isValid {
            get {
                return !string.IsNullOrWhiteSpace(Title);
            }
        }


Display Item x Of y, using Value Converter

Modify Mainwindow.xaml to display Item x of y in the StatusBar:
        <views:ToDo x:Name="vwToDo" DockPanel.Dock="Top" />
        <StatusBar DockPanel.Dock="Bottom" x:Name="status">
            Item <TextBlock Text="{Binding ElementName=vwToDo, 
                Path=DataContext.CurrentPosition, Converter={StaticResource posConverter}}" />
            Of <TextBlock Text="{Binding ElementName=vwToDo, Path=DataContext.Count}" />
        </StatusBar>


Also add to this file the Resource posConverter:
    <window.Resources>
        <models:PositionConverter x:Key="posConverter" />
    </window.Resources>


If we didn't include the Converter then it would display Item 0 Of 3. So create the file PositionConverter.cs (that the resource refers to) in the Models folder:
using System;
using System.Windows.Data;

namespace ToDoApplication.Models {
    [ValueConversion(typeof(int), typeof(int))]
    class PositionConverter : IValueConverter {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
            return (int)value + 1;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
            return (int)value - 1;
        }
    }
}


IValueConverter Interface :MSDN

ValueConverters can be much more complicated than this but, for our simple example, all we need to do is convert the numbers 0,1,2,3,.. from the model to 1,2,3,4,.. for display in the window.

DataTemplates and DataTemplate.Triggers

We will display a red border for High-priority items, and an aqua-border for Low-priority. Modify ToDo.xaml to add a DataTemplate and DataTemplate.Triggers.
    <UserControl.Resources>
        <ObjectDataProvider x:Key="dataFromEnum" MethodName="GetValues"
                            ObjectType="{x:Type system:Enum}">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="models:Priorities" />
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
        <DataTemplate x:Key="priorityTemplate">
            <Border Name="priorityBorder" BorderBrush="Red" 
                    BorderThickness="0" Margin="8,10">
                <ComboBox SelectedValue="{Binding Path=/Priority, Mode=TwoWay}" Margin="2"
                  ItemsSource="{Binding Source={StaticResource dataFromEnum}}">
                </ComboBox>
            </Border>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding Path=/Priority, Mode=TwoWay}">
                    <DataTrigger.Value>
                        <models:Priorities>High</models:Priorities>
                    </DataTrigger.Value>
                    <Setter TargetName="priorityBorder" Property="BorderThickness" Value="1" />
                </DataTrigger>
                <DataTrigger Binding="{Binding Path=/Priority, Mode=TwoWay}">
                    <DataTrigger.Value>
                        <models:Priorities>Low</models:Priorities>
                    </DataTrigger.Value>
                    <Setter TargetName="priorityBorder" Property="BorderBrush" Value="Aqua" />
                    <Setter TargetName="priorityBorder" Property="BorderThickness" Value="1" />
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    </UserControl.Resources>


In the same file replace the ComboBox with a ContentControl that uses the DataTemplate:
        <ContentControl Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" 
                        Content="{Binding}"
                        ContentTemplate="{StaticResource priorityTemplate}" />


A DataTemplate is similar to a ControlTemplate but specifically intended for the presentation of data.

Content="{Binding}" effectively forwards the current binding-context (the ToDoList) to the template; the template then refers to '/Priority' from this context.

WPF has a number of different Triggers. A DataTrigger is

MSDN said:

..a trigger that applies property values or performs actions when the bound data meets a specified condition.


In our case, if the Priority is either High or Low it changes the border applied around the ComboBox. (This is not a very dramatic effect, but it demonstrates the principles.)

Serializing Objects

Only a slight modification to ToDo.cs is required to enable us to save, and load, a list of ToDo items:
    [Serializable()]
    public class ToDo : INotifyPropertyChanged {
        [field:NonSerialized]
        public event PropertyChangedEventHandler PropertyChanged;


We add the Serializable attribute to the class. However, we don't want it to attempt to serialize the event, so we exclude it. When we load (de-serialize) the list later the event will be instantiated anew: it doesn't need to be preserved.

In principle, serializing any objects is straight-forward, just requiring the Serializable attribute. In practice, and depending how complex the objects are, we might need to exclude a number of items from the process, and possibly supply them with default values when the objects are de-serialized.

Serialization :MSDN

Modify ToDoList.cs to add Serial and DeSerial methods. Also add an Exit method as well: we will remove the earlier version from the code-behind.
        public void Serial(string filename = "todos.bin") {
            try {
                using (Stream stream = File.Open(filename, FileMode.Create)) {
                    BinaryFormatter bin = new BinaryFormatter();
                    bin.Serialize(stream, this.ToArray<ToDo>());
                }
            } catch (IOException) {
                throw;
            }
        }
        public void DeSerial(string filename = "todos.bin") {
            try {
                using (Stream stream = File.Open(filename, FileMode.Open)) {
                    BinaryFormatter bin = new BinaryFormatter();
                    this.Clear();
                    ToDo[] aList;
                    aList = (ToDo[])bin.Deserialize(stream);
                    foreach (var item in aList) {
                        this.Add(item);
                    }
                    this.MoveFirst();
                }
            } catch (IOException) {
                throw;
            }
        }

        public void Exit() {
            Application.Current.Shutdown();
        }


The Serial and DeSerial methods will read and write to a file named 'todos.bin' by default.

Although we have excluded an event from the serializing process, if we attempt to directly serialize our ObservableCollection there are a number of issues, mainly because of the inclusion of Command objects. So instead we use ToArray to simplify the object that we are serializing. However, this does mean that when we de-serialize we have to manually Clear our list and Add items to it one-by-one.

It is technically possible to serialize our collection (without ToArray), so that we could de-serialize in one step, without iterating. It requires quite a bit of work though and probably isn't worth the hassle.

Commands to Load and Save (and Exit)

Add the following enum to the bottom of ToDoList.cs (but within the namespace):
    public enum Major { Save, Load, Exit }


Rather than modifying NavigationCommand.cs to include these options, we create a new file MajorCommand.cs in the Models folder. These commands aren't related to navigation and I want to keep NavigationCommand re-usable. (I mentioned before that perhaps Add and Delete aren't navigation commands either, but we could always move them to another ICommand if we wanted to.)

MajorCommand.cs

Spoiler

Notice that saving won't happen if there is an empty title:
    case Major.Save:
        foreach (ToDo item in _list) {
            if (string.IsNullOrEmpty(item.Title)) {
                MessageBox.Show("One of the Titles is empty.",
                    "ToDo Application");
                return;
            }
        }


This doesn't indicate, or take us to, the ToDo item that has an empty Title (although it's probably the current item). When data validation is studied and applied then the user probably wouldn't get to this stage (wouldn't be able to press Save) until the current item has valid data.




It is worth pointing out that we are not using CanExecute in this Command. Some advocate not using this, and therefore not disabling buttons on the window. This disabling can be an irritation for the user, and it is often much simpler just to allow the buttons to be pressed and then decide whether the command can be carried out.

However, I prefer to use CanExecute with the navigation buttons, as there is little point in the user clicking Previous when there is no previous item.




Now we can add Command instances for these in ToDoList.cs. Here is the full code for that file:

ToDoList.cs (full code)

Spoiler

Finally, we can hook-up the Commands in Mainwindow.xaml, and remove the previous version of Exit.

Mainwindow.xaml (full code)

Spoiler

We can also remove the code-behind the MainWindow:

Mainwindow.xaml.cs (full code)

Spoiler

Now both Mainwindow.xaml and ToDo.xaml have negligible code-behind - just the above code that sets the DataContext for our View.

You may have noticed a pattern: using code-behind to test features, or to quickly get something up-and-running, then over a period removing this code. I am probably not alone in taking this approach.

You should be able to run the Application now, and to save, and later load, ToDo lists.

Brief Introduction to MVVM

I will not attempt to describe, or define, the Model-View-ViewModel (MVVM) pattern here, merely to discuss how it might, or could, relate to our Application.

Here are some links that I encountered, although they vary in quality and depth:

Model View ViewModel :wiki

WPF Apps With The Model-View-ViewModel Design Pattern
WPF/MVVM Quick Start Tutorial
Getting Started with MVVM in WPF
Problems and Solutions with Model-View-ViewModel
Implementing the MVVM Pattern Using the Prism Library 5.0 for WPF

The last link in particular I consider quite useful, even though it relates to a Prism Library.




From the last link:

To summarize, the view has the following key characteristics:

  • The view is a visual element, such as a window, page, user control, or data template. The view defines the controls contained in the view and their visual layout and styling.
  • The view references the view model through its DataContext property. The controls in the view are data bound to the properties and commands exposed by the view model.
  • The view may customize the data binding behavior between the view and the view model. For example, the view may use value converters to format the data to be displayed in the UI, or it may use validation rules to provide additional input data validation to the user.
  • The view defines and handles UI visual behavior, such as animations or transitions that may be triggered from a state change in the view model or via the user's interaction with the UI.
  • The view's code-behind may define UI logic to implement visual behavior that is difficult to express in XAML or that requires direct references to the specific UI controls defined in the view.

To summarize, the view model has the following key characteristics:

  • The view model is a non-visual class and does not derive from any WPF base class. It encapsulates the presentation logic required to support a use case or user task in the application. The view model is testable independently of the view and the model.
  • The view model typically does not directly reference the view. It implements properties and commands to which the view can data bind. It notifies the view of any state changes via change notification events via the INotifyPropertyChanged and INotifyCollectionchanged interfaces.
  • The view model coordinates the view's interaction with the model. It may convert or manipulate data so that it can be easily consumed by the view and may implement additional properties that may not be present on the model. It may also implement data validation via the IDataErrorInfo or INotifyDataErrorInfo interfaces.
  • The view model may define logical states that the view can represent visually to the user.

The model has the following key characteristics:

  • Model classes are non-visual classes that encapsulate the application's data and business logic. They are responsible for managing the application's data and for ensuring its consistency and validity by encapsulating the required business rules and data validation logic.
  • The model classes do not directly reference the view or view model classes and have no dependency on how they are implemented.
  • The model classes typically provide property and collection change notification events through the INotifyPropertyChanged and INotifyCollectionchanged interfaces. This allows them to be easily data bound in the view. Model classes that represent collections of objects typically derive from the ObservableCollection<T> class.
  • The model classes typically provide data validation and error reporting through either the IDataErrorInfo or INotifyDataErrorInfo interfaces.
  • The model classes are typically used in conjunction with a service or repository that encapsulates data access and caching.




Following the MVVM pattern would essentially mean creating a new ViewModels folder and moving things around, and adding one or two new classes, maybe with an abstract class (or two) thrown in.

An important consideration though, is that our Application is very small, and currently only has a single View. The MVVM pattern author himself, John Gossman, acknowledges that implementing MVVM may be overkill for simple UI operations - or a small application in our case.

For our application ToDoList.cs is effectively both part of the Model and constitutes our ViewModel. From the last-quoted link:

Quote

The view model in the MVVM pattern encapsulates the presentation logic and data for the view. It has no direct reference to the view or any knowledge about the view's specific implementation or type. The view model implements properties and commands to which the view can data bind and notifies the view of any state changes through change notification events. The properties and commands that the view model provides define the functionality to be offered by the UI, but the view determines how that functionality is to be rendered.


To follow the pattern ToDoList.cs (in a ViewModels folder) should expose only those properties from the Model that a View needs. Proxy properties are usually used to achieve this:
public string Description {
    get { 
        return this.UnderlyingModelInstance.Description; 
    }
    set {
        this.UnderlyingModelInstance.Description = value;
        this.RaisePropertyChangedEvent("Description");
    }
}


This can lead to quite cumbersome code for a complex model. This code is taken from the second-to-last link which also discusses alternative approaches.

Our current version of ToDoList.cs exposes entire ToDo items (via the collection). This isn't necessarily breaking the MVVM pattern, but it isn't following it closely.

Attached Image

Looking at our current structure, what belongs in Models, or ViewModels or Views?

Currently, I will just suggest that ToDoList.cs and MajorCommand.cs could be moved to a ViewModels folder. I might prefer to keep NavigationCommand.cs in the Models folder, because of my aim to make this, and MyObservableCollection, a re-usable unit.

It could also be argued that PositionConverter.cs could be moved to the Views folder, as it is fully disassociated from the Model. However, some might insist that this folder should only contain XAML.

Our Application has one MainWindow so there is no need to move it into the Views folder. For a much bigger Application there probably wouldn't be one Window that would be considered the main window.

I don't consider it worth making these changes just for the sake of it. That is, not until we have, or intend to have, more than one View.

Next Steps

It is, of course, entirely up to you how you progress from here. I will just outline what I am considering, in case it is useful to you.

Fully implementing Data Validation. Particularly explicit validation of an entire ToDo item.

Adding new Views and implementing the MVVM pattern. I haven't decided whether to implement MVVM first, and then add Views, or to create Views afterwards. (Probably make a start with MVVM, then add a View, then continue to implement the pattern.)

Creating a Navigation Window so that we can switch between different views, perhaps by clicking a Button or Hyperlink in a panel to the left.

More work with ControlTemplates and DataTemplates. DataTemplates can be used more extensively with CollectionViews, using a ListView or DataGrid. I could then explore Sorting, Filtering and Grouping.

There are many other topics of course:

  • DataGrid and TreeView
  • 3D Rendering
  • Animation and StoryBoards
  • Media Elements
  • Drag and Drop
  • Creating Documents
  • Resource Dictionaries
  • XML Data Binding
  • ADO.NET Data Binding


Part 5, Data Validation

This post has been edited by andrewsw: 13 June 2014 - 02:41 PM


Is This A Good Question/Topic? 0
  • +

Page 1 of 1