Page 1 of 1

WPF Build An Application 3, More Commands & Validation

#1 andrewsw  Icon User is online

  • It's just been revoked!
  • member icon

Reputation: 3822
  • View blog
  • Posts: 13,544
  • Joined: 12-December 12

Posted 18 May 2014 - 02:01 PM

This part fills-in some details, such as menu-options and a Delete button. (It doesn't introduce a major new topic.)

Part 1 Part 2

Features Added:

  • Add a Delete Command
  • Hook Commands to MenuBar and ToolBar
  • Display Count of Items in StatusBar
  • Add an Exit Command Using ApplicationCommands
  • Data Validation of the Title
  • Validate ToDo Item (an introduction)


Attached Image

The full, current, code is provided at the end of this Part.

Add a Delete Command

Modify MyObservableCollection.cs to add a Delete enum:
    public enum Navigation {
        First, Previous, Next, Last, Add, Delete
    }


Modify NavigationCommands.cs to include these two additional cases:
        // in the CurrentChanged event's switch:
        case Navigation.Delete:
            // there must be at least one ToDo item left
            _canExecute = (_list.Count > 1 && _list.CurrentPosition >= 0);
            break;

        // in the Execute's switch:
        case Navigation.Delete:
            _list.RemoveAt(_list.CurrentPosition);
            break;


(Count is a pre-existing property of an ObservableCollection.)

I am using the condition that there must be at least one ToDo item in the list. This won't always be appropriate. If we don't use this condition then it requires a bit more work, because the Window is still visible and gives the false impression that you can enter details for a ToDo item - but there isn't one! (It also causes some issues with Validation.)

(I suppose you could delete all items, but then immediately start a new one. This seems a little odd, and might be problematic.)




If we want to continue with our aim of making the Commands (and MyObservableCollection) re-usable then it requires some thought. Firstly, do Add and Delete belong in a list of NavigationCommands? I don't think this is a major issue though, because we could create a second Command-class specifically for commands like these (ChangeCommands?).

Secondly, as already mentioned, should it be possible for the Delete command to delete the last-remaining item in the list? To allow for both possibilities the model would have to expose some property that the Command can read to decide whether this deletion can occur. (Or some action should be triggered in the model when the last item is removed.)




Modifying ToDoList.cs is straight-forward:
    public ICommand CommandAdd { get; private set; }

    CommandDelete = new NavigationCommand(this, Navigation.Delete);


Hook Commands to MenuBar and ToolBar

We can add a Button for Delete in ToDo.xaml easily enough:
        <Button Content="First" Name="btnFirst" Command="{Binding Path=CommandFirst}" />
        <Button Content="Previous" Name="btnPrevious" Command="{Binding Path=CommandPrevious}" />
        <Button Content="Next" Name="btnNext" Command="{Binding Path=CommandNext}" />
        <Button Content="Last" Name="btnLast" Command="{Binding Path=CommandLast}" />
        <Button Content="_New" Name="btnNew" Command="{Binding Path=CommandAdd}" />
        <Button Content="_Delete" Name="btnDelete" Command="{Binding Path=CommandDelete}" />


(Notice that I removed the CommandParameters as they aren't used.)

I also modified App.xaml to reduce the size of the Buttons:
        <Style TargetType="Button" BasedOn="{StaticResource controlStyle}">
            <Setter Property="HorizontalContentAlignment" Value="Center" />
            <Setter Property="MinWidth" Value="50" />
            <Setter Property="Margin" Value="5,12" />
            <Setter Property="Padding" Value="5" />
        </Style>


Modify Mainwindow.xaml to use the same Commands in the Menu and ToolBar:
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="_FILE">
                <MenuItem Header="_Load" />
                <MenuItem Header="_Save" />
            </MenuItem>
            <MenuItem Header="_EDIT">
                <MenuItem Header="_New" Command="{Binding ElementName=vwToDo, Path=DataContext.CommandAdd}" />
                <MenuItem Header="_Delete" Command="{Binding ElementName=vwToDo, Path=DataContext.CommandDelete}" />
            </MenuItem>
            <MenuItem Header="_VIEW">
                <MenuItem Header="_Previous" Command="{Binding ElementName=vwToDo, Path=DataContext.CommandPrevious}" />
                <MenuItem Header="_Next" Command="{Binding ElementName=vwToDo, Path=DataContext.CommandNext}" />
            </MenuItem>
        </Menu>
        <ToolBarTray DockPanel.Dock="Top">
            <ToolBar>
                <Button Content="New" Command="{Binding ElementName=vwToDo, Path=DataContext.CommandAdd}" />
            </ToolBar>
        </ToolBarTray>


We need to bind to our User Control (vwToDo) and access the Commands via its DataContext (the ToDoList).

If you run the application then the menu and toolbar items are disabled in the same way as the navigation-buttons, based on CanExecute.

The Load and Save menu-options don't do anything currently.

Display Count of Items in StatusBar

This should be straight-forward because Count exists as a property of an ObservableCollection and automatically updates. Change the StatusBar in Mainwindow.xaml to:
        <StatusBar DockPanel.Dock="Bottom">
            Total Tasks: <TextBlock Text="{Binding ElementName=vwToDo, Path=DataContext.Count}" />
        </StatusBar>


I have been working on getting the status-bar to display "Item 3 Of 5" but haven't resolved it yet.

Add an Exit Command Using ApplicationCommands

I use ApplicationCommands.Close to implement an Exit Command. I also decided to put this in the code-behind.

Why code-behind? Aren't we trying to avoid this?

  • It is easy to set up and therefore easy to change later.
  • It provides a quick demonstration of the use of ApplicationCommands.

I decided that the Exit command will just exit the application, not involving our model at all. If, for example, we didn't want to execute this command unless the ToDoList had been saved (or a decision was made not to save it) then I would remove it from the code-behind and integrate it into the Model (or ViewModel).

Modify Mainwindow.xaml to the following:
    <window.CommandBindings>
        <CommandBinding Command="ApplicationCommands.Close" Executed="CommandClose_Executed" 
                        CanExecute="CommandClose_CanExecute">
        </CommandBinding>
    </window.CommandBindings>
    <window.InputBindings>
        <KeyBinding Command="Close" Key="F4" Modifiers="Alt" />
    </window.InputBindings>
    <window.Resources>
    </window.Resources>
    <DockPanel>
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="_FILE">
                <MenuItem Header="_Load" />
                <MenuItem Header="_Save" />
                <MenuItem Header="E_xit" Command="Close" />
            </MenuItem>


WPF Tutorial - Command Bindings and Custom Commands

InputBindings provide a keyboard-shortcut for the Command. They can provide other triggers (InputGestures) to execute the Command.

Right-click the text "CommandClose_Executed" and choose Navigate to Event Handler to create the initial code for this handler, in the code-behind. Do the same for "CommandClose_CanExecute".

Mainwindow.xaml.cs:
// default using statements omitted
using ToDoApplication.Models;

namespace ToDoApplication {
    /// <summary>
    /// Interaction logic for Mainwindow.xaml
    /// </summary>
    public partial class MainWindow : Window {

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

        private void MainWindowLoaded(object sender, RoutedEventArgs e) {
            vwToDo.DataContext = new ToDoList();
            vwToDo.txtTitle.Focus();
        }

        private void CommandClose_Executed(object sender, ExecutedRoutedEventArgs e) {
            Application.Current.Shutdown();
        }
        private void CommandClose_CanExecute(object sender, CanExecuteRoutedEventArgs e) {
            e.CanExecute = true;
            e.Handled = true;
        }
    }
}


As you can see, there is very little code required, and it is completely UI-based; that is, nothing to do with our Model. (I also took the opportunity to Focus() on the Title-TextBox when the Window is loaded.)

e.Handled = true prevents the Command from bubbling further up the UI element-chain.

You can run and test this menu-option, pressing Alt-F4 should also exit the application.

Data Validation of the Title

We will add a simple validation-requirement that the Title cannot be empty, nor more than 30 characters. (The approach used here is also discussed in my earlier WPF Build a Window tutorial.) The TextBox will be decorated (adorned) with an error message when it loses the focus; that is, when the user tabs away from the TextBox. (See the earlier screenshot.)

The simple validation used here is not sufficient to prevent the user from navigating to a different ToDo item. It does, however, prevent the bound-property (the Title) from being updated (whilst HasError is true).

Create a new class TitleValidator.cs in the Models folder:
using System;
using System.Windows.Controls;

namespace ToDoApplication.Models {
    class TitleValidator : ValidationRule {
        public override ValidationResult Validate(object value, 
                System.Globalization.CultureInfo cultureInfo) {
            if (value == null || String.IsNullOrEmpty(value.ToString()))
                return new ValidationResult(false, "There must be a Title.");
            else {
                if (value.ToString().Length > 30)
                    return new ValidationResult(false, "Maximum of 30 characters.");
            }
            return ValidationResult.ValidResult;
        }
    }
}


Add the following to App.xaml which provides a ControlTemplate which will be used as a Validation.ErrorTemplate to display the error message (decorating the TextBox). It also provides a Style to display a ToolTip when HasError is true.
    <ControlTemplate x:Key="validationTemplate">
        <DockPanel>
            <StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
                <Grid Width="12" Height="12" Margin="0,0,5,0">
                    <Ellipse Width="12" Height="12" Fill="Red" 
                             HorizontalAlignment="Center" VerticalAlignment="Center" />
                    <TextBlock Foreground="White" FontWeight="Heavy" FontSize="8" 
                               HorizontalAlignment="Center" VerticalAlignment="Center" 
                               TextAlignment="Center" 
                               ToolTip="{Binding ElementName=TheErrorElement, 
                        Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" Text="X" />
                </Grid>
                <AdornedElementPlaceholder DockPanel.Dock="Top" x:Name="TheErrorElement"  />
            </StackPanel>
            <TextBlock Foreground="Red" FontWeight="12" Margin="20,3,3,3" 
                        Text="{Binding ElementName=TheErrorElement, 
                        Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" />
        </DockPanel>
    </ControlTemplate>
    <Style x:Key="styTextMissing" TargetType="TextBox" BasedOn="{StaticResource controlStyle}">
        <Setter Property="Padding" Value="3" />
        <Style.Triggers>
            <Trigger Property="IsFocused" Value="True">
                <Setter Property="Background" Value="LightYellow" />
            </Trigger>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip" 
                    Value="{Binding RelativeSource={RelativeSource Self},
                    Path=(Validation.Errors)[0].ErrorContent}" />
            </Trigger>
        </Style.Triggers>
    </Style>


The Style will display the Tooltip when pointing over the TextBox. There is also a ToolTip in the ControlTemplate which displays when pointing over the red circle. We don't really need both but this demonstrates alternatives.

Now we can modify the Title-TextBox in ToDo.xaml to use these features:
    <Label Grid.Row="0" Grid.Column="0" Content="Title" />
    <TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3" x:Name="txtTitle" 
             Validation.ErrorTemplate="{StaticResource validationTemplate}" 
             Style="{StaticResource styTextMissing}">
        <TextBox.Text>
            <Binding Path="/Title" Mode="TwoWay" UpdateSourceTrigger="LostFocus">
                <Binding.ValidationRules>
                    <models:TitleValidator></models:TitleValidator>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>


If you run this you will see that it isn't perfect. If you add a new ToDo item the Title is initially empty but doesn't display an error message, which is fine. But clicking in and out of this box (to lose focus) won't display it either. If you type something, then delete it and tab out, it will display the error.

This can be resolved by putting the default text 'Your Title Here.' for every new item, but it is a pain to have to delete it each time. Alternatively, we can ignore this small quirk and rely on validating the entire ToDo item.

Note that there remains another quirk. If you start a new ToDo item and type a Title, but don't then tab away from the TextBox, the value stored is still null, as the updating of the bound-property hasn't taken place. A temporary, but not complete, solution is to change the UpdateSourceTrigger from LostFocus to PropertyChanged. That is, as the user types in the TextBox.

Validate ToDo Item (an introduction)

Data Validation is a large topic and I am only scraping the surface of it in this part of the tutorial.

DataValidation for a single property, such as requiring there to be a Title, works reasonably well, but this doesn't help where we need to validate properties in combination. (DataValidation is intended to work with a single value.)

Homework: Create a new DataValidation class named, for example, SensibleDate that will not allow a date earlier than some arbitrary, but sensible, date such as 1st Jan 2000. Apply this to all three date-controls. Either use our existing ControlTemplate or create a new one. You'll need to allow for null-values though.

For our application:

  • The dates could be any reasonable date, but the DueDate should be after the StartDate.
  • Similarly, the CompletedDate should occur after the StartDate. (A ToDo item that was completed before its StartDate probably isn't worth keeping.)
  • If Completed is ticked then there should be a CompletedDate.

Ticking Completed could insert the current date (if CompletedDate hasn't alerady been entered) but this is a feature of the application rather than being directly related to validation.

(Should StartDate be required for a ToDo item, as well as a Title? This is a business decision.)

Validating an entire item (Explicit validation) isn't as complete a feature in WPF as it was in WinForms with its ErrorProvider Class. However, as a first step in this process, it might be sensible to have an isValid property in the ToDo class, that we can also make use of in our NavigationCommands. Add this in ToDo.cs:
    internal bool isValid {
        get {
            return !string.IsNullOrWhiteSpace(Title);
        }
    }


Now modify the Execute method in NavigationCommands.cs to use this property:
    public void Execute(object parameter) {
        if (_request != Navigation.Delete && !_list[_list.CurrentPosition].isValid) {
            MessageBox.Show("Current item is not valid.", "ToDo Application");
            return;
        }
        switch (_request) {


(If they are deleting the current item there is no need to check if it is valid.)

The MessageBox perhaps doesn't belong in the Command; the Command should either execute, or not execute. It is included here to demonstrate that isValid is doing its job.




Full Code Listing

Mainwindow.xaml

Spoiler

Mainwindow.xaml.cs

Spoiler

App.xaml

Spoiler

Views\ToDo.xaml

Spoiler

Views\ToDo.xaml.cs

Spoiler

Models\ToDo.cs

Spoiler

Models\MyObservableCollection.cs

Spoiler

Models\ToDoList.cs

Spoiler

Models\NavigationCommand.cs

Spoiler

Models\TitleValidator.cs

Spoiler

This post has been edited by andrewsw: 21 May 2014 - 01:00 PM


Is This A Good Question/Topic? 0
  • +

Page 1 of 1