There are only about 7 hours we have left in 2010 and this will be my last blog post for this year. For those of you who are not familiar with the new RadDomainDataSource control for Silverlight, here is my introductory blog post. This one describes how to load data with the new control and this one is about performing CRUD. Having read these three blogs might lead you to the next logical question: What about MVVM support?
I truly believe that every time someone places an UI element or a control in his view model, a baby kitten dies somewhere. So what can we do about this?
Luckily, the class that RadDomainDataSource internally uses as its view is public and can be used directly in your view models. You can learn about the relationship between the RadDomainDataSource control and the QueryableDomainServiceCollectionView here.
The architecture of my sample project is fairly simple. I have a page with a grid, a pager and a bunch of other configuration controls. All of these UI elements are bound to a common view model which is the data context of the root element:
- <UserControl x:Class="SilverlightApplication1.MainPage"
- 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"
- xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
- xmlns:e="clr-namespace:SilverlightApplication1.Web.Services"
- xmlns:my="clr-namespace:SilverlightApplication1"
- mc:Ignorable="d"
- d:DesignHeight="300" d:DesignWidth="400">
- <UserControl.Resources>
- <my:CustomersViewModel x:Key="CustomersViewModel"/>
- </UserControl.Resources>
- <Grid DataContext="{StaticResource CustomersViewModel}">
- <Grid.ColumnDefinitions>
- <ColumnDefinition Width="*"/>
- <ColumnDefinition Width="250"/>
- </Grid.ColumnDefinitions>
- <Grid Name="mainGrid" Grid.Column="0">
- <Grid.RowDefinitions>
- <RowDefinition Height="*" />
- <RowDefinition Height="Auto" />
- </Grid.RowDefinitions>
- <telerik:RadGridView
- x:Name="radGridView"
- Grid.Row="0"
- ItemsSource="{Binding View}"
- IsBusy="{Binding IsBusy}"
- ShowGroupPanel="False"
- RowIndicatorVisibility="Collapsed"
- IsFilteringAllowed="False"
- CanUserSortColumns="False"
- AutoGenerateColumns="False"
- IsReadOnly="True">
- <telerik:RadGridView.Columns>
- <telerik:GridViewDataColumn DataMemberBinding="{Binding Country}"/>
- <telerik:GridViewDataColumn DataMemberBinding="{Binding City}"/>
- <telerik:GridViewDataColumn DataMemberBinding="{Binding ContactName}" Header="Contact"/>
- <telerik:GridViewDataColumn DataMemberBinding="{Binding ContactTitle}" Header="Title"/>
- <telerik:GridViewDataColumn DataMemberBinding="{Binding CompanyName}" Header="Company"/>
- </telerik:RadGridView.Columns>
- </telerik:RadGridView>
- <telerik:RadDataPager x:Name="radDataPager"
- Grid.Row="1"
- Margin="0, 0, 0, 1"
- Source="{Binding View}"
- DisplayMode="All"
- IsTotalItemCountFixed="True"/>
- </Grid>
- <StackPanel Grid.Column="1" Margin="3">
-
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition/>
- <RowDefinition/>
- </Grid.RowDefinitions>
- <Grid.ColumnDefinitions>
- <ColumnDefinition/>
- <ColumnDefinition/>
- </Grid.ColumnDefinitions>
- <CheckBox Content="Auto"
- Margin="1"
- VerticalAlignment="Center"
- VerticalContentAlignment="Center"
- IsChecked="{Binding AutoLoad, Mode=TwoWay}"/>
- <telerik:RadButton Grid.Column="1"
- Content="Refresh"
- Margin="1"
- Padding="10,2"
- Command="{Binding LoadCommand}"/>
- <TextBlock Grid.Row="1"
- Text="Page Size"
- VerticalAlignment="Center"
- Margin="1" />
- <telerik:RadNumericUpDown Grid.Row="1"
- Grid.Column="1"
- Margin="1"
- Value="{Binding PageSize, Mode=TwoWay}"
- Minimum="0"
- IsInteger="True"/>
- </Grid>
-
- <TextBlock Text="Sort by" Margin="0,10,0,0" FontWeight="Bold" />
- <TextBlock Text="Country" />
- <RadioButton Content="Ascending"
- GroupName="CountrySort"
- IsChecked="{Binding IsCountryAscendingChecked, Mode=TwoWay}"/>
- <RadioButton Content="Descending"
- GroupName="CountrySort"
- IsChecked="{Binding IsCountryDescendingChecked, Mode=TwoWay}"/>
- <RadioButton Content="None"
- GroupName="CountrySort"
- IsChecked="{Binding IsCountryNoneChecked, Mode=TwoWay}"/>
- <TextBlock Text="Then by" Margin="0,10,0,0" FontWeight="Bold" />
- <TextBlock Text="City" />
- <RadioButton Content="Ascending"
- GroupName="CitySort"
- IsChecked="{Binding IsCityAscendingChecked, Mode=TwoWay}"/>
- <RadioButton Content="Descending"
- GroupName="CitySort"
- IsChecked="{Binding IsCityDescendingChecked, Mode=TwoWay}"/>
- <RadioButton Content="None"
- GroupName="CitySort"
- IsChecked="{Binding IsCityNoneChecked, Mode=TwoWay}"/>
-
- <TextBlock Text="Filter by" Margin="0,10,0,0" FontWeight="Bold" />
- <TextBlock Text="Contact Title" Margin="0,5,0,0" />
- <telerik:RadComboBox ItemsSource="{Binding ContactTitles}"
- SelectedValue="{Binding SelectedContactTitle, Mode=TwoWay}"/>
- </StackPanel>
- </Grid>
- </UserControl>
All of the business logic happens inside this view model, which makes it perfect for unit testing. A central part of the view model is an instance of the QueryableDomainServiceCollectionView. The view itself and several of its properties are exposed as properties of the view model, so various UI elements can be bound to them:
- private readonly QueryableDomainServiceCollectionView<Customer> view;
-
- public CustomersViewModel()
- {
- NorthwindDomainContext context = new NorthwindDomainContext();
- EntityQuery<Customer> getCustomersQuery = context.GetCustomersQuery();
- this.view = new QueryableDomainServiceCollectionView<Customer>(context, getCustomersQuery);
- //...
- }
To illustrate this, let’s take a look at how the page size can be changed by the user. The view contains a RadNumericUpDown bound to the PageSize property of the view model:
- <telerik:RadNumericUpDown Grid.Row="1"
- Grid.Column="1"
- Margin="1"
- Value="{Binding PageSize, Mode=TwoWay}"
- Minimum="0"
- IsInteger="True"/>
The view model simply forwards the page changing logic between the view and the model. In my case there is no additional complex logic involved, but I your real-life projects may be more complex than this:
- public int PageSize
- {
- get
- {
- return this.view.PageSize;
- }
- set
- {
- if (this.view.PageSize != value)
- {
- this.view.PageSize = value;
- this.OnPropertyChanged("PageSize");
- }
- }
- }
The view model gives you unlimited opportunities to inject custom logic in both directions. As a matter of fact, RadDomainDataSource is built in a very similar way. It aggregates an instance of the QueryableDomainServiceCollectionView and simply forwards information forth and back. So, for example, when you go and change RadDomainDataSource.PageSize to 10, it will simply change the PageSize of its view to 10. In fact, you can write these two lines of code and they will do absolutely the same thing:
- radDomainDataSource.PageSize = 10;
- radDomainDataSource.DataView.PageSize = 10;
You can totally bypass RadDomainDataSource and work directly with the view. That’s the reason why in my previous blog post I have described RadDomainDataSource as just a “XAML-friendly thin wrapper over the QDSCV”. Come to think of it, when you are developing your very own view models that will aggregate a QDSCV, you are kind of writing your very own personal RadDomainDataSource that does exactly what you need. With the only difference that RadDomainDataSource is a control and your view model is not.
In the sample project that I have attached you will also see how to use buttons with commands, how to listen for changes in the QDSCV, and how to retrieve and expose distinct values. So attach your debuggers and hit F5.
May your source code compile and your unit tests pass in 2011! I wish you a Happy New Year!
Download Sample Project