The video links require Microsoft Silverlight 1.0 or later. If you do not have it, you will be prompted to install it when you click one of the links or you can download Silverlight here. Windows XP or Vista required.
Cipher Text 'How To' Video
Introduction
This article uses the WPF Password Manager, Cipher Text application as fertile ground for exploring the WPF UI Design Pattern, Model-View-ViewModel or MVVM. The application sports many cool features and some WPF goodness.
In this article I'll cover a number of MVVM coding scenarios and techniques along with using Cipher Text.
Background
My first .NET application after graduating from the SetFocus .NET Masters Program in 2003 was a .NET 2.0 Windows Forms password manager. I've been using this same program for securing my passwords for the last five years.
I've been studying and writing WPF Applications using the WPF UI Design Pattern, MVVM. I'm sold that using MVVM for WPF Line of Business (LOB) applications is the way to go.
Now I wanted to see if I could create a fluid and dynamic application using MVVM. Basically, I wanted to take MVVM out for a test drive, push the envelope and see where it took me. The time spent on Cipher Text was time very well spent and I hope you can learn more about MVVM from this article.
This article is about MVVM coding scenarios and techniques as opposed to the application used to surface them. I do hope that you like the application too.
Designer Developer Workflow: as I was writing Cipher Text its UI was plain and simple. After finishing the application, I took it into Microsoft Expression Blend for a makeover. I liked the workflow of completing the application before starting the beautification process.
During application development I made extensive use of my XAML Power Toys Visual Studio Add-In. If you have not yet seen XAML Power Toys, please visit my blog and download it.
Let's cover the Cipher Text application first and then dive into MVVM.
WPF Password Manager, Cipher Text Application
Application Features
- Data stored in encrypted file
- Flexible password generator
- Instant full text search feature
- Nine pre-established data entry forms (can be modified at run-time)
- Run-time modification of a record's shape and field behavior (add, change and remove fields)
- Run-time configurable case correction rules
- Dynamic and rich field validation based on assigned field behavior
- Single click copy of any data to the clipboard
- Nice WPF UI
- "How To" usage video for getting the most of Cipher Text
- Single file XCOPY deployment
Application Requirements
- .NET Framework 3.5 with SP1 applied
Application Introduction
The application stores its data in an encrypted file using symmetric encryption employing Triple Data Encryption Standard algorithm (TripleDES). While this is not an article on the cryptography namespaces in .NET, it does demonstrate the reading and writing of a serialized object graph to an encrypted file.
This application can be used "as is" to store your passwords. However please note two very important items:
- Once decrypted, your data is in memory and can be written to the Windows swap file by the operating system. If your computer is then compromised, a hacker could potentially view data in your swap file. (I'm still trying to figure out how to allocated memory in a managed program that does not get written to the Windows swap file, when I do, I'll update the application.)
- If you forget your password, your data is lost. There are no hacks, tricks, workarounds or magic fairy dust to open your data file. A brute force dictionary hack is probably the only way to decrypt the file. Please don't lose or forget your password.
Back in 2003 when I wrote the first version the application met my needs. Then a year later, banks and web sites started asking me to assign a security question to my accounts or for me to provide my mother's maidan name. Since my original application didn't have the ability to update the shape of a record without an application recompile, I put the extra information in a notes field. Now my bank asks for three security questions and an icon to view.
After five years of daily application usage I knew what features I wanted to add. So when I set out to write this program, I needed the ability to change the shape of a record at run-time. I needed to be able to add, change or remove fields. I needed to be able to change which fields are required, which fields have case correction applied and which fields are validated against a set of rules. I also wanted to have instant full text searching of all my data.
Application UI
There is a wind blowing through UI Designer spaces that is pushing UI's to be less busy; to clear UI adorners and affordances until the user is actually using them. For example, hiding a ComboBox's familiar down arrow until the user mouses over the ComboBox. I'm not sure I'm on board with all this yet, but decided to open the door and allow a gentle breeze to influence my design.
Login
 |
 |
First Time Use Login |
Login |
The first time the application is opened the password for the application is created. By design, the first time password is displayed in clear text so the user can view it while typing it. The standard login form masks the password as its being entered.
Getting Started
After logging in the first time, the above application is displayed.
The set of buttons on the left control filtering and permit a new record to be added to the database. The text at the very top is darkened until the user mouses over it. The data viewer on the right allows for full text searching and data viewing.
Category Buttons
 |
 |
 |
Not Selected |
Selected |
Mouse Over |
Only one category can be selected at a time. When selected, only that category's records are displayed. The 'All Records' category displays all records in the database and is selected by default when the application opens.
When the user mouses over the category, the category is no longer dimmed and a green plus icon is display in the button. Clicking the green plus icon displays an empty record that the user can edit and save to the database. (This is an example of hiding a UI element until it is needed or available for use. I think it produces a cleaner UI.)
Filtering
After selecting a category, the title of the data viewer (area in top red box) changes to reflect the current category. The filter TextBox displays a watermark indicating that no filter is currently applied.
Once the user beings entering text the title of the data viewer changes, indicating a filtered list is being displayed. Also a button appears at the end of the filter text box that allows for single click clearing of the filter TextBox.
Edit Record
Each category has a default edit form. The user can customize each edit form on the fly. The red "!
" indicates a field needs attention before the data can be saved. If the user mouses over the red "!
" a ToolTip appears, explaining the required corrective action.
When the yellow Key Icon is clicked it opens the create password dialog.
The bottom black bar organizes the possible actions for this form. Actions that are not currently permitted are grayed out.
When the form is valid, the Save command will be enabled. (In case you're wondering, that is the real customer support number for Amazon.com.)
Each of the field tags except Date Created and Date Modified are hyperlinks that when clicked, copy the corresponding field to the clipboard. Additionally when the field tags are dragged, a drag and drop operation is initiated. This allows the dragging of data to other applications or copying data to other fields.
After saving the data the record is added to the database and the edit form is closed.
When editing the Amazon record, the URL field was filled in. When a record with a URL is displayed in the data viewer, the title text is white. Notice that the Expression Blend License record title is gray. When a title field is white, this indicates it's a hyperlink that when clicked will take the user to the web site.
Also, when editing the Amazon record, the User Name and Password fields were entered. When a record with a User Name and/or a Password is displayed in the data viewer, the user and/or key icons are displayed. When these icons are clicked the information is copied to the clipboard.
Modify Form
Form modification has two states. First the form can be in a modification state and one or more fields can be in a validation rule modification state. The above image shows the form in a modification state. When the entire form is valid (including data), the form can be returned to a normal form state by clicking the 'Normal Form' hyperlink in the lower left corner. The form can also be saved when it is valid.
The title and notes fields can't be modified.
Field Validation Rule Modification
The above image shows the phone field tag modified to indicate that the phone number is the Customer Support number. The phone fields allow for notes to be added after the phone number, so I could have indicated this number is the customer support by adding this information after the phone number.
When a field is expanded as the above Customer Support field is, if the field is not valid, the green check mark icon will be disabled, preventing the field from being collapsed until the fields are valid.
The second phone field has been marked for delete and will be removed when the form is saved.
The Max Len field sets the maximum length for the text in the data field.
When Is Required is checked, this makes the data field required entry.
The Type determines the role the field plays in the application and how it is validated.
- Credit Card Number - validates the credit card number
- Email - validates the email
- IP Address - validates the IP Address
- Password Primary - displays the key icon to the right of the field data, allowing a password to be generated. The Password Primary field is also causes the data viewer to display the key icon.
- Password Secondary - displays the key icon to the right of the field data, allowing a password to be generated.
- Plain Text - no rule applied or action taken
- Routing Number - validates the routing number
- URL Primary - validates the URL. The URL Primary field also causes the title field in the data viewer to be displayed in white and become a hyperlink.
- URL Secondary - validates the URL
- User Name Primary - causes the data viewer to display the user icon
- User Name Secondary - no rule applied or action taken
The Case determines how the application modifies entered data in the field.
- Lower - changes data case to lower case
- None - no changes made to data
- Outlook Phone - attempts to format the data like Microsoft Outlook. Also applies Proper case to text after the phone number.
- Proper - applies proper casing to entered text. Proper casing rules can also be added. See below section.
- Upper - changes data case to upper case
Modifying Casing Rules
Casing rules as very simple. Once the data field has been changed to proper case, the above rules are applied. For example, let's say your data is, "125 110th St Se". You want the address displayed as, "125 110TH St SE". In order words, you want the abbreviation for South East to be displayed in upper case.
The first rule looks for " Se " and replaces it with " SE ". Notice that the white space in the fields. You wouldn't want to replace all occurrences of "Se" with "SE" as this would cause problems.
The Modify Casing Rules form is fully editable. Each row can be changed without placing the form in an edit mode. If you want to remove a casing rule, click the delete button. If you delete a rule and want it back, simply press the Cancel hyperlink, the form will close and no changes written to the database.
The only validation requirement is that the, "Look For" string and "Replace With" string are the same length.
The green plus icon allows adding a new rule.
When all rules are valid, the Save command is enabled. If any rules are not valid (i.e. not the same length) the Save command will be disabled.
Modified Form
The above form has been modified and data entry completed. Extra fields have been removed and the Phone field tag changed to Customer Service.
Top Hyperlink Bar
The top hyperlink bar text is normally gray. When the mouse is moved over it, the text foreground changes to white.
- Float On Top - when clicked, floats the application on top of other windows. This is useful when dragging and dropping text from the application to other applications like a web browser.
- Change Password - displays the Change Password dialog for changing the password
- 'How To Video' - is a hyperlink to the instructional video for the application. This video requires Silverlight 1.0 or later to view.
- Code Project Article - is a hyperlink to this article
- Karl's Blog - is a hyperlink to my blog
Change Password
The Change Password dialog requires the current password be entered and a new password entered before the Save command is enabled. When the Save command is executed, the database is encrypted using the new password.
Category Buttons
Most of you know Karl was not blessed with any design skills. For this application, I wanted to break my usual pattern of Menus, ToolBars, StatusBar, square buttons and attempt to author an application that has some WPF goodness. I spent a good bit of time trying to design a layout that I liked (or thought would not look like chimps throwing feces). I started looking around and found a cool demo for the XCEED WPF products. I used the demo application for inspiration and went to work.
The behavior I wanted from the category buttons was to only allow one button to be selected at a time. This sounds like a job for the RadioButton or ToggleButton. I chose the RadioButton.
One of the most awesome features of WPF is the ability to re-template a control to have the look, feel and behavior you need without requiring the control to be subclassed.
The category button is a multi-layer button with cool features and mouse over effects. The multiple layers allow elements to render behind other opaque layers and show through them. The multiple layers combined with WPF triggers also makes it very easy to dim the button when it's not selected or the mouse is not over it. The layering is achieved by placing UI Elements as children of a Grid in a single row.
I used Microsoft Expression Blend to create the below control template. Blend makes it very easy to draw paths, layer controls, set gradient values and create required triggers. I also like the zoom & pan features of blend that makes it super easy to zoom into view any portion of your scene.
The below CardTypeCommandView
UserControl
is our category button and is also the DataTemplate
for the CardTypeCommandViewModel
.
<UserControl
x:Class="CardTypeCommandView"
xmlns:local="clr-namespace:CipherText"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<UserControl.Effect>
<DropShadowEffect
Color="#FF545454"
ShadowDepth="5" />
</UserControl.Effect>
<UserControl.Resources>
<local:IsAllRecordsConverter
x:Key="isAllRecordsConverter" />
</UserControl.Resources>
<RadioButton
x:Name="rdo"
IsTabStop="False"
GroupName="CardTypeCommands"
Command="{Binding Path=FilterCommand}"
CommandParameter="{Binding Path=CardType}"
IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"
Background="{x:Null}"
BorderBrush="{x:Null}"
Foreground="{x:Null}">
<RadioButton.Template>
<ControlTemplate
TargetType="{x:Type RadioButton}">
<Border
Height="70"
Width="90"
Margin="0,0,0,0"
BorderBrush="#FF2F2E2E"
BorderThickness="1,1,1,1"
Padding="0,0,0,0"
x:Name="border"
ClipToBounds="True"
Visibility="Visible"
CornerRadius="7,0,7,7"
RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform
ScaleX="1"
ScaleY="1" />
</TransformGroup>
</Border.RenderTransform>
<Border.Background>
<LinearGradientBrush
EndPoint="0.016,0.137"
StartPoint="0.965,0.695">
<GradientStop
Color="#FF000000"
Offset="0.147" />
<GradientStop
Color="#FFA9A9A9"
Offset="1" />
</LinearGradientBrush>
</Border.Background>
<Grid
Background="{x:Null}"
ClipToBounds="True">
<Image
Stretch="None"
Source="{Binding Path=CardType.Icon}"
HorizontalAlignment="Stretch"
Margin="0,0,0,0"
VerticalAlignment="Stretch" />
<Border
Visibility="Visible"
Background="#83000000"
CornerRadius="7,0,7,7"
x:Name="bdrDarken"
d:IsHidden="True" />
<Path
Stretch="Fill"
Stroke="{x:Null}"
StrokeThickness="0"
Margin="-0.602,-0.41,-3.718,-0.043"
ClipToBounds="True"
SnapsToDevicePixels="True"
Data="M87.662375,57.350326 C95.267257,46.41438 ... ...
...">
<Path.Fill>
<LinearGradientBrush
EndPoint="0.278,0.121"
StartPoint="0.535,0.728">
<GradientStop
Color="#FF000000"
Offset="0.469" />
<GradientStop
Color="#FF606060"
Offset="1" />
</LinearGradientBrush>
</Path.Fill>
</Path>
<Rectangle
Fill="#FF000000"
Stroke="#FF000000"
RadiusX="7"
RadiusY="7"
VerticalAlignment="Bottom"
Height="21"
Opacity="0.36"
StrokeThickness="1"
Margin="0,0,0,0"
Visibility="Visible" />
<TextBlock
Text="{Binding Path=CardType.CardTypeName}"
Margin="0,3.5"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Foreground="#FFFFFFFF" />
<Button
x:Name="btnAdd"
Visibility="Collapsed"
Margin="0,35,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
IsTabStop="False"
Command="{Binding Path=NewCommand}"
CommandParameter="{Binding Path=CardType}"
ToolTip="Click to add this card type to the database."
Style="{StaticResource gridButtonStyle}">
<Image
Stretch="Uniform"
Height="16"
Width="16"
Source="{StaticResource addImage}" />
</Button>
</Grid>
</Border>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition
Property="IsMouseOver"
Value="True" />
<Condition
Property="IsChecked"
Value="False" />
</MultiTrigger.Conditions>
<MultiTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Duration="00:00:00.3"
Storyboard.TargetName="border"
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"
To="1.1" />
<DoubleAnimation
Duration="00:00:00.3"
Storyboard.TargetName="border"
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"
To="1.1" />
</Storyboard>
</BeginStoryboard>
</MultiTrigger.EnterActions>
<MultiTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Duration="00:00:00.3"
Storyboard.TargetName="border"
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"
To="1" />
<DoubleAnimation
Duration="00:00:00.3"
Storyboard.TargetName="border"
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"
To="1" />
</Storyboard>
</BeginStoryboard>
</MultiTrigger.ExitActions>
<Setter
Property="Visibility"
TargetName="bdrDarken"
Value="Collapsed" />
</MultiTrigger>
<Trigger
Property="IsChecked"
Value="True">
<Setter
Property="RenderTransform">
<Setter.Value>
<ScaleTransform
ScaleX="1.1"
ScaleY="1.1" />
</Setter.Value>
</Setter>
<Setter
Property="Visibility"
TargetName="bdrDarken"
Value="Collapsed" />
</Trigger>
<Trigger
Property="IsMouseOver"
Value="True">
<Setter
Property="Visibility"
TargetName="btnAdd"
Value="Visible" />
</Trigger>
<DataTrigger
Binding="{Binding Path=CardType.CardTypeName,
Converter={StaticResource isAllRecordsConverter}}"
Value="True">
<Setter
Property="Visibility"
TargetName="btnAdd"
Value="Collapsed" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</RadioButton.Template>
</RadioButton>
</UserControl>
The triggers provide a scale animation when the mouse is moved over or leaves the category button. A trigger also displays the green plus Add icon when the mouse is over the button unless the category is the, "All Records" category. The IsAllRecordsConverter
provides the "All Records" testing and set the Visibility
is of the Add icon in the above DataTrigger
.
All buttons data bind to an ICommand property on the DataContext, in this case the CardTypeCommandViewModel.
Exploring MVVM Coding Scenarios
MVVM is a WPF UI Design pattern; it's a set of guidelines to help solve a problem. I like MVVM because it facilitates application Unit Testing provided for free in Visual Studio 2008 Professional. I'm not a purest by any means. I do what is required to get the work done. Delivering applications that can be Unit Tested and maintained over time is my primary objective and always overrides strict adherence to a pattern or line of thinking. So like my great friend and mentor Josh Smith says, "put away the flame throwers" and let's get into some code.
Rather than walk the reader though the entire application, I thought it best to pick some coding scenarios and see how MVVM can help solve the problem.
For those that are brand new to MVVM, please see the references at the bottom of this article. I will also post an Introduction to MVVM on my blog soon.
RelayCommand
The RelayCommand
was originally posted by MVVM Master Josh Smith in his Crack .NET application. Josh and I also used it in our Creating an Internationalized Wizard in WPF Code Project article.
The RelayCommand
comes in two flavors, Generic and non-Generic. When using the Generic version, it allows the developer to specify the Type that the Command Parameter
is. In the non-Generic version the Command Parameter
is Type Object
.
The RelayCommand
radically reduces the number of command classes an application would require without it by it.
For example, a ViewModel
needs to expose four commands to the UI. Your first thought may be to create four unique command objects to handle the requirement. A simpler and much cleaner approach is to expose the commands as read-only properties on the ViewModel
of Type ICommand
. In the property getter create a RelayCommand
to handle the command Execute
and command CanExecute
methods.
Notice the lazy instantiation of the private command fields.
Public ReadOnly Property GeneratePasswordCommand() As ICommand
Get
If _cmdGeneratePasswordCommand Is Nothing Then
_cmdGeneratePasswordCommand = _
New RelayCommand(AddressOf GeneratePasswordExecute, _
AddressOf CanGeneratePasswordExecute)
End If
Return _cmdGeneratePasswordCommand
End Get
End Property
Public ReadOnly Property RemoveFieldCommand() As ICommand
Get
If _cmdRemoveFieldCommand Is Nothing Then
_cmdRemoveFieldCommand = _
New RelayCommand(AddressOf RemoveFieldExecute)
End If
Return _cmdRemoveFieldCommand
End Get
End Property
In the above code snippet, the first ICommand
property has assigned the Execute
delegate to the GeneratePasswordExecute
method and the CanExecute
delegate to the CanGeneratePasswordExecute
method.
Note the naming convention I've adopted. After coding ViewModel's with more than a few commands, appending "Execute" to the Execute
command name makes it very easy to locate the method in IntelliSense. For the CanExecute
method names, I've pre-pended, "Can" and appended "Execute" for the same reason.
The second ICommand
property does not have a CanExecute
delegate assigned. This command is enabled all the time. The RelayCommand
takes care of returning True
when called, which in effects keeps the UI Element that is bound to the ICommand
enabled all the time.
In WPF there is a static class, CommandManager
that handles the task of automatically enabling and disabling commands. This feature (like many others) comes at a price. When using many RoutedCommands
in an application, CommandManager
can causes some performance issues because the CommandManager
raises the RequerySuggested
RoutedEvent
a lot. For example, every time a key is pressed this event and its corresponding preview event travels through the WPF Element Tree.
The ICommand
pattern here does not use RoutedCommands
. When the UI Element executes a command, the RelayCommand
delegates are executed directly.
If however you want to take advantage of the CommandManager
automatically enabling and disabling your UI Element commands you have to sign up for this service.
The RelayCommand
in this article has a modification that makes registering with the CommandManager SuggestedRequery
event optional. Note the overloaded constructor in the above RelayCommand
class diagram. If the developer does not supply a delegate for the CanExecute
method, the RelayCommand
does not register for the SuggestedRequery
event.
The RelayCommand
is very performant. I have a test application with 100 unique buttons with the Command
property bound to an ICommand
property with a backing RelayCommand
with automatic enabling and disabling enabled with no effect on performance.
Tip: You can use the RelayCommand
even if you're not using MVVM. The pattern is the same. This coding technique can give you the power and flexibility of WPF commands without the CommandBinings
and all the associated overhead.
List of Commands Scenario
The developer has a list of objects and wants to associate one of more actions with each object. The objects need to be rendered as a list in the UI. The objects can be sourced from a database or a static set of application data.
One solution made easy by MVVM is to wrap each object in a ViewModel
. Then associate that wrapper ViewModel
with a DataTemplate
and allow the WPF Resources system to look up the correct DataTemplate
for the data object (ViewModel
). Wrapping each object in a ViewModel
allows the DataTemplate
(View
) to data bind required commands to the ViewModel
and for reshaping of data if required.
The concrete example of this scenario in Cipher Text is the category buttons on the left side of the application.
The driving data for this scenario is the CardTypes
collection in the Database
object. The CardTypes
collection defines each of the application card types by specifying its title, icon and default form fields.
We now use MVVM to reshape the CardType
data making it selectable and associating two commands with it.
The below block of code from the ApplicationMainWindowViewModel
class. When the CardTypeCommands
property is first accessed by the UI, it creates a ReadOnlyCollection
of Type CardTypeCommandViewModels
. The LoadCardTypeCommands
function is passed in the constructor of the ReadOnlyCollection
.
Public ReadOnly Property CardTypeCommands() As _
ReadOnlyCollection(Of CardTypeCommandViewModel)
Get
If _objCardTypeCommands Is Nothing Then
_objCardTypeCommands = LoadCardTypeCommands
End If
Return _objCardTypeCommands
End Get
End Property
Private Function LoadCardTypeCommands() As _
ReadOnlyCollection(Of CardTypeCommandViewModel)
Dim obj As New List(Of CardTypeCommandViewModel)
For Each objCardType As CardType In Application.DataBase.CardTypes
obj.Add(New CardTypeCommandViewModel( _
New RelayCommand(Of CardType)(AddressOf NewExecute), _
New RelayCommand(Of CardType)(AddressOf FilterExecute), _
objCardType))
Next
Dim objAllRecordsViewModel As CardTypeCommandViewModel = _
(From c In obj _
Where c.CardType.CardTypeName = _
Application.STR_ALLRECORDS).FirstOrDefault
If objAllRecordsViewModel IsNot Nothing Then
objAllRecordsViewModel.IsSelected = True
End If
Return New ReadOnlyCollection(Of CardTypeCommandViewModel)(obj)
End Function
Private Sub FilterExecute(ByVal objCardType As cardtype)
Me.DataBaseViewModel.SetCardTypeFilter(objCardType.CardTypeName)
End Sub
Private Sub NewExecute(ByVal objCardType As CardType)
Me.EditingRecord = True
Me.DataEditorViewModel = _
New DataEditorViewModel(objCardType)
AddHandler DataEditorViewModel.RequestClose, _
AddressOf RequestCloseEventHandler
End Sub
<RadioButton GroupName="CardTypeCommands"
Command="{Binding Path=FilterCommand}"
CommandParameter="{Binding Path=CardType}"
IsChecked="{Binding Path=IsSelected, Mode=TwoWay}" ... >
The LoadCardTypeCommands
function iterates through the DataBase.CardTypes
collection, creating one CardTypeCommandViewModel
for each CardType
. Notice that the RelayCommands
are of Type CardType
. This makes it clean when the FilterExecute
and NewExecute
methods are called by assigning a concrete Type for the method parameter instead of Object
that would require casting.
The RadioButton XAML code snippet shows CardTypeCommandViewModel
being consumed by the CardTypeCommandView
.
Take note of the GroupName property. By assigning each RadioButton to the same GroupName
we get the behavior we are looking for, single selected Category button.
Binding to a Command
and passing a Command Parameter
in XAML could not be easier.
By binding to the IsChecked
to the IsSelected
property, the button can be selected in code on the UI.
Doesn't this just feel natural? Model exposed by a ViewModel to a View. ViewModel adapting Model to the View. View responding to data changes in the ViewModel. ViewModel responding to data changes from the Model and View.
List of Commands Scenario Note
Normally when a ViewModel
exposes an ICommand
property, it also handles the Execute
and CanExecute
delegates.
In the above scenario, the NewExecute
and FilterExecute
methods are members of the ApplicationMainWindowViewModel
class. The ApplicationMainWindowViewModel
class handles the adding and filtering operations for the application so it makes sense to want this class to handle command Execute
methods.
Pointing the CardTypeCommandViewModel
command Execute
method delegates to the ApplicationMainWindowViewModel
class methods makes for a cleaner design.
If we didn't use this technique, the CardTypeCommandViewModel
would either have to raise an event when a command was executed or have knowledge of the ApplicationMainWindowViewModel
and call methods on it.
Rending CardTypeCommandViewModel
<Window.Resources>
<DataTemplate DataType="{x:Type local:CardTypeCommandViewModel}">
<local:CardTypeCommandView />
</DataTemplate>
<Style x:Key="alternatingListViewItemStyle"
TargetType="{x:Type ListViewItem}">
<Style.Triggers>
<Trigger Property="ItemsControl.AlternationIndex"
Value="0">
<Setter Property="Margin" Value="16,7" />
</Trigger>
<Trigger Property="ItemsControl.AlternationIndex"
Value="1">
<Setter Property="HorizontalAlignment"
Value="Right" />
<Setter Property="Margin" Value="16,-20" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<ListView
Margin="0,49,0,0"
Background="{x:Null}"
BorderThickness="0"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
ItemContainerStyle="{StaticResource alternatingListViewItemStyle}"
AlternationCount="2"
ItemsSource="{Binding Path=CardTypeCommands}" />
The ListView's
ItemsSource
is a ReadOnlyCollection(Of CardTypeCommandViewModel)
. When WPF renders the ListView C
ontent
, it looks up the DataTempate
for the CardTypeCommandViewModel
and finds it in the above DataTemplate
. The CardTypeCommandView
is the cool button we described earlier.
Getting the buttons to stagger within the ListView
took some thought. I was going to write my own panel to lay the buttons out. Then I remembered the new AlternationCount
property and wrote the above alternatingListViewItemStyle
Style
. By simply adjusting the margins of the buttons in the Trigger
, I got the effect I wanted without having to author a panel control.
Showing and Hiding Views and Focus Setting Scenario
The developer wants to display an edit form in response to a command. When the edit form is brought into view, Keyboard Focus needs to be set to the first UI Control on the form.
We could display the form in another window; could add the form to a TabControl or we could layer the window on top of other UI Elements in the application.
The concrete example of this scenario in Cipher Text is the record edit form that is displayed when the user double clicks a record in the data viewer.
ApplicationMainWindowView.xaml Code Snippet
<Grid Grid.Column="2" Margin="0,25,7,7">
<local:DatabaseView DataContext=
"{Binding Path=DataBaseViewModel}" />
<local:DataEditorView DataContext=
"{Binding Path=DataEditorViewModel}" />
</Grid>
DataEditorView.xaml Code Snippet
<UserControl
x:Class="DataEditorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CipherText"
TargetUpdated="VisibilityChanged_EventHandler"
Visibility="{Binding
Path=DataEditorVisibility,
NotifyOnTargetUpdated=True,
FallbackValue={x:Static Visibility.Hidden}}">
-->
To accomplish the simple task of showing and hiding a View we will be using data binding (big surprise right). Again we see the simplicity of MVVM in action. A View responding to data changes in the ViewModel.
The top snippet has two Views
in the same Grid Row
. The bottom View DataEditorView
will be rendered on top of DatabaseView
if it is visible. The DataEditorView
is initially hidden so the DatabaseView
will be in view in the UI.
When the DataEditorView.DataContext
Is Nothing
(nulll), the above Visibility
property would assume its FallbackValue
of Hidden
because the binding returned Nothing
(null).
The only thing left is to set focus to the first field in the View when the View
is displayed. Here we assign the TargetUpdate
event handler to below VisibilityChanged_EventHandler
. This event will be raised each time the Visibility
property is changed.
This event handler gives Keyboard Focus to the Title TextBox each time the View's
Visibility
changes to Visible
.
Private Sub VisibilityChanged_EventHandler( _
ByVal sender As System.Object, _
ByVal e As System.Windows.Data.DataTransferEventArgs)
e.Handled = True
If Me.Visibility = Windows.Visibility.Visible Then
Keyboard.Focus(Me.txtTitle)
End If
End Sub
Oh NO! You have code in the View code behind. Yes I do and it's all good. MVVM like most design patterns is a set of guidelines, not hard and fast rules. Design patterns when followed keep you out of trouble and prevent you from reinventing the wheel so they are a good thing.
Josh and I spoke about this a good bit when we wrote the Creating an Internationalized Wizard in WPF application. If the code is UI specific and does not require Unit Testing then having the code in the code behind is fine. Have a look at the code behind files, you'll see some UI specific code in some.
My guidance is to follow the MVVM design pattern in most cases. On occasion a scenario will present itself where deviation may make sense. Weigh the pros and cons, make a decision and move on. Remember, you and your team members are responsible for delivering and maintaining the code.
In the next scenario you'll see two more examples of UI specific code in the code behind.
Data Driven Dynamic Form Scenario
The developer needs to render a data driven dynamic form. The form needs its behavior determined at run-time based on the source data.
The concrete example of this scenario in Cipher Text is the record editor. Each field in the form is data driven along with the field validation and casing rules.
FieldEditorViewModel & FieldEditorView
The FieldEditorViewModel
adapts the CardField
Model
to the FieldEditorView
. The CardField
class implements IDataErrorInfo
. This class has built in data validation rules and casing rules that run based on the FieldType
and FieldCase
properties.
AvailableFieldTypes
and AvailableFieldCases
properties are ReadOnlyCollections
of OptionViewModels
. OptionViewModel
is a Generic class that Josh and I authored and described in our Creating an Internationalized Wizard in WPF article. OptionViewModel
specializes in wrapping values from Enumerations
, providing human readable descriptions and customized sorting of values. Additionally it simplifies programming with Flags Enumerations
. OptionViewModel
is another example of the power and simplicity of MVVM. A ViewModel effortlessly adapting a Model to the UI.
There are three Boolean properties that determine the state of the UI.
- InSchemaEditingMode - This value is toggled when the user clicks the, "Modify Form" hyperlink on the data editor form. The
DataEditorViewModel
holds the collection of FieldEditorViewModels
. The DataEditorViewModel
handles the "Modify Form" command and iterates through the FieldEditorViewModels
setting this property value. UI Elements on the FieldEditorView
are bound to this property and display when the value is True
.
- IsFieldInEditMode - This value is toggled when the user clicks the, "Edit Field" icon. UI Elements on the
FieldEditorView
are bound to this property and display when its value is True
.
- IsMarkedForDelete - This value is toggled when the user clicks the red, "Remove Field" icon or the green check, "Restore Field" icon. When the value is
True
, a gray border overlays the field as in the below image.
Below are three instances of the FieldEditorView
in three different states. The top Address field is expanded but is invalid because the Address field has not yet been filled in and it's a required field. The Phone field has been marked for delete. The Web Site fields are complete and can be contracted when the green check icon is clicked.
The green icon is really a XAML asset that I made that has a trigger data bound to the IsEnabled property of the button. The button's Command CanExecute
method checks the Error
property on the CardField
to determine if the Command
is enabled and the field can be collapsed.
The FieldEditorView
is completely data driven. This unique and complex, yet powerful application feature was simple to design and code because of WPF's impressive data binding capabilities. This UI is also super simple to Unit Test because it's data driven.
Hiding Adorners
The red "!
" indicates an invalid field and are displayed in the Adorner Layer of the TextBox as defined in the Validation.ErrorTemplate
. However, if the user elects to delete a field that has one or more displayed adorners those adorner need to be removed otherwise it would display on top of the gray border.
The below OnIsMarkedForDeleteUpdated
method is called when the Visibility
property of the gray border is changed. If the FieldEditorViewModel.IsMarkedForDelete
is True
then the border is displayed and any adorners on the FieldEditorView
are removed.
Private Sub OnIsMarkedForDeletetUpdated( _
ByVal sender As System.Object, _
ByVal e As System.Windows.Data.DataTransferEventArgs)
e.Handled = True
If _objFieldEditorViewModel Is Nothing Then
Exit Sub
End If
If _objFieldEditorViewModel.IsMarkedForDelete Then
Dim objAdornerLayer As AdornerLayer = AdornerLayer.GetAdornerLayer(Me)
If objAdornerLayer IsNot Nothing Then
ClearAdorders(objAdornerLayer, Me.txtFieldData)
ClearAdorders(objAdornerLayer, Me.txtFieldSortOrder)
ClearAdorders(objAdornerLayer, Me.txtFieldTag)
ClearAdorders(objAdornerLayer, Me.txtMaximumLength)
End If
End If
End Sub
Private Sub ClearAdorders(ByVal objAdornerLayer As AdornerLayer, _
ByVal objUIElement As UIElement)
Dim objAdorder() As Adorner = objAdornerLayer.GetAdorners(objUIElement)
If objAdorder IsNot Nothing Then
For Each obj As Adorner In objAdorder
objAdornerLayer.Remove(obj)
Next
End If
End Sub
FieldEditorView Commands
The Commands
except for the GeneratePasswordCommand
all simply changed data values in the ViewModel
. The View reacts to changes in the ViewModel's
data to correctly render itself.
The two below command methods contract and expand the field in and out of edit mode.
You'll notice that I make a sanity check by calling the corresponding CanExecute
method. This is a good practice since there is no guarantee that another developer didn't just make a direct call to the method.
Private Sub ContractEditFieldExecute(ByVal param As Object)
If CanContractEditFieldExecute(param) Then
Me.IsFieledInEditMode = False
OnPropertyChanged("CardField")
End If
End Sub
Private Sub EditFieldExecute(ByVal param As Object)
If CanEditFieldExecute(param) Then
Me.IsFieledInEditMode = True
OnPropertyChanged("CardField")
End If
End Sub
WPF and MVVM
This application shows off the symbionic couple WPF & MVVM. This natural application development style typified by smooth data flow from the Model to the ViewModel that is consumed by the View simplifies WPF application development and Unit Testing.
If you're coming from Windows Forms, ASP.NET or another event driven programming platform, it takes a little adjustment to grasp the full power and simplicity to code a WPF MVVM application where the UI reacts to data changes rather than event handlers that modify named controls. Give yourself time, write simple applications, you'll be so glad you did. Then convert a small application that you're familiar with, add the Unit Tests to the new application and take that to work on Monday and show off your new developer skills.
One area I've been paying special attention to is, memory profiling. I use the Red-Gate Ants Profiler. Ants has helped me understand .NET and memory management. You may see areas in my code where I have code something that at first glace you think, "why did he do that?" It was probably because I was memory profiling and found that some prudent clean up of objects prevented objects from hanging around in memory.
History
- 28 December 2008 : Initial Release