Alessandro Del Sole's Blog

/* A programming space about Microsoft® .NET® */
posts - 159, comments - 0, trackbacks - 0

My Links

News

Your host

This is me! This space is about Microsoft® .NET® and Microsoft® Visual Basic development. Enjoy! :-)

These postings are provided 'AS IS' for entertainment purposes only with absolutely no warranty expressed or implied and confer no rights.

Microsoft MVP

My MVP Profile

I'm a VB!

Watch my interview in Seattle

My new book on VB 2015!

Pre-order VB 2015 Unleashed Pre-order my new book "Visual Basic 2015 Unleashed". Click for more info!

My new book on LightSwitch!

Visual Studio LightSwitch Unleashed My book "Visual Studio LightSwitch Unleashed" is available. Click the cover!

Your visits

Follow me on Twitter!

CodePlex download Download my open-source projects from CodePlex!

Article Categories

Archives

Post Categories

.NET Framework

Blogroll

Help Authoring

Microsoft & MSDN

Setup & Deployment

Visual Basic 2005/2008/2010

WPF: Introducing the Model-View-ViewModel pattern for Visual Basic 2010 developers - part 10 (and last!)

This is definitely the last blog post of the introductory series about the Model-View-ViewModel in WPF with Visual Basic 2010, particularly against the ADO.NET Entity Framework. I will explain how to build the user interface and how to bind it to ViewModels. Although this is the last blog post of the series, this does not mean that I will no longer discuss MVVM with VB 2010. On the contrary this will be an important topic for this blog, but for the sake of clarity I will distinguish topics in different blog posts and today we will complete the sample application. You can find the download link at the bottom of the page. I will go quite fast now, since I have to show you a lot of code and most of the concepts have been explained previously. Nothing difficult, just things you already learned through the series.

The ErrorTemplate

As you may remember, in part 5 we implemented data validation rules inside the Order class, also implementing the IDataErrorInfo interface. In order to make the UI show correctly data validation errors, we need an ErrorTemplate for controls of type TextBox (which will be used in the main View). The following code has been already described in part 4 and it has to be added to the Application.xaml file:

 

<Application x:Class="Application"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Views\MainWindow.xaml">
    <Application.Resources>
        <Style x:Key="ButtonStyle" TargetType="Button">
            <Setter Property="Width" Value="80"/>
            <Setter Property="Height" Value="40"/>
            <Setter Property="Margin" Value="5"/>
        </Style>
 

        <Style TargetType="Control" x:Key="myErrorTemplate">
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <DockPanel LastChildFill="True">
                            <TextBlock DockPanel.Dock="Right" 
                       
 Foreground="Red"
                        FontSize="14pt" 
                       
 FontWeight="ExtraBold">*
                            </TextBlock>
                            <Border BorderBrush="Red" BorderThickness="4">
                                <AdornedElementPlaceholder Name="myControl"/>
                            </Border>
                        </DockPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip"
                Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                Path=(Validation.Errors)[0].ErrorContent}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
        <Style TargetType="TextBox" BasedOn="{StaticResource myErrorTemplate}"/>
 
        <Style TargetType="Label">
            <Setter Property="Foreground" Value="White"/>
        </Style>
    </Application.Resources>
</Application>

The template will display a red border around TextBox controls whose content is invalid, until the user fixes the data.

The main View

According to the figure shown in Part 5, the user interface is composed of the following ingredients:

  • a main Grid nesting:
    • a ListBox for the customers list 
    • a Grid containing controls for displaying information from a single order information
    • a StackPanel containing action buttons

With that said the first thing to do is defining the main Window as follows, also specifying references to the application's assembly and adding a graceful gradient background: 

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Northwind Traders Orders Management" Height="508" Width="876" mc:Ignorable="d" 
       
 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
       
 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
   
 xmlns:my="clr-namespace:adsMVVM_EntityFramework_Complete"
    WindowStartupLocation="CenterScreen">
    <Window.Background>
        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="Black" Offset="0" />
            <GradientStop Color="White" Offset="1" />
        </LinearGradientBrush>
    </Window.Background>

Of course the name of the application's assembly may vary depending on the name you assigned to the project. Now let's divide the main Grid into three columns, also assigning an identifier for code-behind interaction: 

    <Grid Name="MainGrid" ShowGridLines="False" >
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200" />
            <ColumnDefinition />
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

Now let's declare the ListBox for the customers list. Pay attention to the data-binding:

        <ListBox Name="ListBox1" Grid.Column="0"  ItemsSource="{Binding Path=CustomerViewSource.View}" DisplayMemberPath="CompanyName"
                 SelectedItem="{Binding Path=Selection, Mode=TwoWay}" />

The control is data-bound (ItemsSource) to the CustomersViewSource.View property from the object that will be assigned to the main Grid's DataContext (that is, the ViewModel). This time controls are bound to the view objects from CollectionViewSources/ListCollectionViews instead of an ObservableCollection. DisplayMemberPath allows specifying the element of the collection that will be displayed. Also notice how the selected item in the ListBox is data-bound to the Selection property from the ViewModel, in both reading and writing. Now it's time to declare the central Grid which contains TextBox and DatePicker controls (the latter are useful for working with dates) in order to represent a single order. Also notice how each control is data-bound to the related property exposed by the data source (which is an instance of the Order class) but most of all how the Grid is populated via the CustomerOrdersView object which exposed by the ViewModel that will be associated to the View in a few moments:

        <Grid Grid.Column="1" Name="Grid1" DataContext="{Binding Path=CustomerOrdersView}" >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Label Content="Order ID:" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="0" Height="23" HorizontalAlignment="Left" Margin="3" Name="OrderIDTextBox" 
                     Text="{Binding Path=OrderID, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
            <Label Content="Customer ID:" Grid.Column="0" Grid.Row="1" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="1" Height="23" HorizontalAlignment="Left" Margin="3" Name="CustomerIDTextBox" 
                     Text="{Binding Path=CustomerID, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
            <Label Content="Employee ID:" Grid.Column="0" Grid.Row="2" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="2" Height="23" HorizontalAlignment="Left" Margin="3" Name="EmployeeIDTextBox" 
                     Text="{Binding Path=EmployeeID, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
            <Label Content="Order Date:" Grid.Column="0" Grid.Row="3" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <DatePicker Grid.Column="1" Grid.Row="3" Height="25" HorizontalAlignment="Left" Margin="3" Name="OrderDateDatePicker" 
                        SelectedDate="{Binding Path=OrderDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="115" />
            <Label Content="Required Date:" Grid.Column="0" Grid.Row="4" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <DatePicker Grid.Column="1" Grid.Row="4" Height="25" HorizontalAlignment="Left" Margin="3" Name="RequiredDateDatePicker" 
                        SelectedDate="{Binding Path=RequiredDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="115" />
            <Label Content="Shipped Date:" Grid.Column="0" Grid.Row="5" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <DatePicker Grid.Column="1" Grid.Row="5" Height="25" HorizontalAlignment="Left" Margin="3" Name="ShippedDateDatePicker" 
                        SelectedDate="{Binding Path=ShippedDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="115" />
            <Label Content="Ship Via:" Grid.Column="0" Grid.Row="6" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="6" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipViaTextBox" 
                     Text="{Binding Path=ShipVia, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
            <Label Content="Freight:" Grid.Column="0" Grid.Row="7" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="7" Height="23" HorizontalAlignment="Left" Margin="3" Name="FreightTextBox" 
                     Text="{Binding Path=Freight, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
            <Label Content="Ship Name:" Grid.Column="0" Grid.Row="8" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="8" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipNameTextBox" 
                     Text="{Binding Path=ShipName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
            <Label Content="Ship Address:" Grid.Column="0" Grid.Row="9" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="9" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipAddressTextBox" 
                     Text="{Binding Path=ShipAddress, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
            <Label Content="Ship City:" Grid.Column="0" Grid.Row="10" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="10" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipCityTextBox" 
                     Text="{Binding Path=ShipCity, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
            <Label Content="Ship Region:" Grid.Column="0" Grid.Row="11" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="11" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipRegionTextBox" 
                     Text="{Binding Path=ShipRegion, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
            <Label Content="Ship Postal Code:" Grid.Column="0" Grid.Row="12" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="12" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipPostalCodeTextBox" 
                     Text="{Binding Path=ShipPostalCode, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
            <Label Content="Ship Country:" Grid.Column="0" Grid.Row="13" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="13" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipCountryTextBox" 
                     Text="{Binding Path=ShipCountry, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true, ValidatesOnDataErrors=True}" VerticalAlignment="Center" Width="120" />
        </Grid>

Remember to set the ValidatesOnDataErrors = True markup inside the TextBox's binding, with regard to the ShipCountry property, which is the one that has validation rules in the model. In order to complete the user interface of the main window we just need to define some buttons and command bindings like this:

        <StackPanel Name="Stack2" Orientation="Vertical" Grid.Column="2" >
            <Button Content="Save changes" 
                   
 Command="{Binding SaveCommand}"
                    Name="SaveButton" Style="{StaticResource ButtonStyle}" />

           <Button Content="Next"
  
                   
 Name="NextButton"
 
                   
 Command="{Binding NextCommand}"
 
                   
 Style="{StaticResource ButtonStyle}" />
            <Button Content="Previous" Command="{Binding PreviousCommand}"
                    Name="PrevButton" Style="{StaticResource ButtonStyle}" />
            <Button Content="Delete order"
                    Name="DeleteButton"
  
                   
 Command="{Binding DeleteCommand}"
 
                   
 Style="{StaticResource ButtonStyle}" />
            <Button Content="New order"
 
                   
 Command="{Binding Path=InsertCommand}"
                    Name="NewButton"
 
                   
 Style="{StaticResource ButtonStyle}" />
            <Button Content="View details"
                    Command="{Binding ViewDetailsCommand}"
                    Name="Button1"
  
                   
 Style="{StaticResource ButtonStyle}"  />
        </StackPanel>
    </Grid>
</Window>

You should easily remember how in MVVM you do not handle the Click event while you associate the Command property to a property of type ICommand exposed by the ViewModel. Now let's switch to the Visual Basic code so that the UI will become ready to perform some actions.

No code? No party!
In the first blog posts of this series we saw how the main Window, which is the first View, contains only code that creates an instance of the ViewModel, assigning such an instance to the Window's DataContext. Now this is the principle: each View can contain also other code, but such a code must be only UI code so it cannot access data or other stuff. In our particular scenario, the Window's code-behind will contain code that creates an instance of the ViewModel but also it will contain code that registers a message that will allow displaying the Order Details window, once it is sent. This is accomplished via the
Register method from the Messenger class. This is finally the code-behind:

Class MainWindow
 
    
Private Sub MainWindow_Loaded(ByVal sender As ObjectByVal e As System.Windows.RoutedEventArgsHandles Me.Loaded
        
Dim startupViewModel As New OrdersViewModel
        Me.MainGrid.DataContext = startupViewModel
 
        
Application.Msn.Register(Application.VIEW_DETAILS_EXECUTE, Sub()
                                                                       
Try
                                                                           'Get the instance of the current order and send the ID to the constructor
                                                                           'of the OrderDetailsView window
                                                                           Dim odv As New OrderDetailsView(CType(startupViewModel.CustomerOrdersViewSource.View.CurrentItem, Order).OrderID)
                                                                           odv.ShowDialog()
                                                                           odv = 
Nothing
                                                                       Catch ex As Exception
                                                                           MessageBox.Show(ex.Message)
                                                                       
End Try
                                                                   End Sub)
    
End Sub
End Class

The following are some important considerations:

  1. The instance of the ViewModel is not assigned to the Window's DataContext but to the Grid's DataContext. This is a particular case, because for hierarchical reasons the various controls are populated starting from the first panel.
  2. The Register method receives, as arguments, the message to be registered and the action to take once the message is sent by the ViewModel. In this case the message has been defined as a constant at the application level, while the action is defined via a statement lambda instead of pointing to a stand-alone delegate via AddressOf (but if you use this older approach, it is correct the same).
  3. With regard to the second argument of Register, the lambda expression retrieves the instance of the selected Order passing its identifier to the constructor of a window named OrderDetailsView, that will be created shortly. Then the code runs this new window. Since this is UI-related code, it can be written at the View level.
  4. About messages exchange: the View registers the message and the application will listen for such a message, then the ViewModel emits the message when the appropriate command is invoked, finally the View intercepts the message and shows the second window.

Once the first View is completed, we can move forward to the second one.

Building the Order Details view

In our sample application, Order Details will be displayed inside a DataGrid in a new Window; for the sake of simplicity, I will not implement any other control but you are free to do it on your own once you understand the logic. Now add a new Window to the Views project subfolder, calling it OrderDetailsView.xaml. When ready, you can also use the new VS 2010 drag'n'drop data-binding feature to make your life easier, by dragging the Order_Details entity from the Data Sources window onto the designer. This will also enable some design-time features for working with data, by the way you need to write the following XAML (which also contains the auto-generated code from Visual Studio after the drag'n'drop):

<Window x:Class="OrderDetailsView"

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

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

    xmlns:my="clr-namespace:adsMVVM_EntityFramework_Complete"

    xmlns:dal="clr-namespace:DAL;assembly=DAL" ShowInTaskbar="True" WindowStartupLocation="CenterScreen"

    Title="OrderDetailsView" Height="300" Width="370" mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:my1="clr-namespace:DAL;assembly=DAL">

    <Window.Resources>

        <CollectionViewSource x:Key="Order_DetailViewSource" Source="{Binding Path=Order_Details}" d:DesignSource="{d:DesignInstance dal:Order_Detail, CreateList=True}" />

    </Window.Resources>

    <Grid DataContext="{StaticResource Order_DetailViewSource}">

        <DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True" ItemsSource="{Binding}" Name="Order_DetailsDataGrid"

                  RowDetailsVisibilityMode="VisibleWhenSelected">

            <DataGrid.Columns>

                <DataGridTextColumn x:Name="ProductIDColumn" Binding="{Binding Path=ProductID}" Header="Product ID" Width="SizeToHeader" />

                <DataGridTextColumn x:Name="UnitPriceColumn" Binding="{Binding Path=UnitPrice, StringFormat=c}" Header="Unit Price" Width="SizeToHeader" />

                <DataGridTextColumn x:Name="QuantityColumn" Binding="{Binding Path=Quantity}" Header="Quantity" Width="SizeToHeader" />

                <DataGridTextColumn x:Name="DiscountColumn" Binding="{Binding Path=Discount, StringFormat=p}" Header="Discount" Width="SizeToHeader" />

            </DataGrid.Columns>

        </DataGrid>

    </Grid>

</Window>

Notice how you can use a CollectionViewSource to set the Grid's DataContext at design-time. Such a data source points to the Order_Details property exposed by the ViewModel. At the code-behind level, all is similar to the main View. In this case we need an explicit constructor, because we need the OrderID as a requisite:

Public Class OrderDetailsView
    Private _orderID As Integer
    Private WithEvents OrderDetailsVM As OrderDetailsViewModel

    
Sub New(ByVal OrderID As Integer
)
        InitializeComponent()
        
' TODO: Complete member initialization 
        Me
._orderID = OrderID
    
End Sub
 
    
Private Sub Window_Loaded(ByVal sender As System.ObjectByVal e As System.Windows.RoutedEventArgsHandles MyBase
.Loaded
        
Me.Title = "Order details for order nr. " & Me
._orderID.ToString
        
Me.OrderDetailsVM = New OrderDetailsViewModel
(_orderID)
        
Me.DataContext = Me
.OrderDetailsVM
        
Application.Msn.Register(Application.VIEW_DETAILS_CLOSE, Sub
()
                                                                     
Me
.Close()
                                                                 
End Sub
)
    
End Sub
End Class

Basically we are registering the message for closing the window but we will not use it effectively. You can remove it, if you wish, or you can add a button bound to the CloseCommand exposed by the ViewModel.

C'mon, let me press F5!
At the end of the work, we can finally run the application. You will get a result similar to the following figure, which also shows the Order Details window running:

This is instead what we get in case of a data validation error:

Conclusions and Download

As I said several times, nothing is perfect and the possibility of improving the code is something that must be considered. By the way, my hope is that this blog post series on MVVM from a Visual Basic 2010 perspective (especially the parts dedicated to the Entity Framework) has been useful as a pure introduction. I will post shortly the full series summary for your convenience. Finally, you can download the full source code from this address. The prerequisites is that you need the Northwind database available on your SQL Server instance (you can change the connection string in order to point to a local database).

Stay tuned for other blog posts on MVVM with VB 2010!

Alessandro

Print | posted on venerdì 13 agosto 2010 18:52 | Filed Under [ Visual Studio 2010 Visual Basic Windows Presentation Foundation ]

Feedback

No comments posted yet.

Post Comment

Title  
Name  
Email
Url
Comment   
Please add 1 and 4 and type the answer here:

Powered by:
Powered By Subtext Powered By ASP.NET