Wednesday, April 21, 2010

Binding ComboBox and AutoComplete Controls in Silverlight DataGrid

Silverlight Data and Controls Series

Introduction

In this blog I am going to again build off a previous blog and dive a little bit deeper into binding with the MVVM pattern, Silverlight and RIA Services. In my first blog on this topic I gave a quick introduction into MVVM and how it can be used with textboxes and a grid. I subsequently wrote this blog which added a custom authentication domain service. I am going to build off the solution developed thus far to bind to more controls. Specifically I am going to discuss how binding DatePicker, ComboBox and AutoCompleteBox in and out of a DataGrid.

One thing I was very impressed with Silverlight of how easy it is to embed any control you want into a grid, including complete custom Silverlight user controls. The only tricky thing is getting some of the properties configured for ComboBox and AutoCompleteBox for Silverlight. It was a little tricky in my situation because I cannot leverage Entity to fill up my Data Transfer Objects (DTO).

Modifications to the Solution

Some classes that I had to Silverlight project were:

  • LookupProvider.cs – this is a class that is responsible for loading up lookup value collections.
  • TitleConvert.cs – this is a class that is used to convert values set in the AutoCompleteBox.
  • TitleDTO.cs – simple DTO class.
Untitled4Untitled5

EmployeeDTO Review

Let us first review the EmployeeDTO object. As you will see the Title is an int value loaded up from the database.

namespace MVVM.Web.Model
{
public class EmployeeDTO
{
[System.ComponentModel.DataAnnotations.Key]
public int ID { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public DateTime HireDate { get; set; }

public int Title { get; set; }
}
}

TitleDTO

Here is the TitleDTO, pretty straight forward.

namespace MVVM.Web.Model
{
public class TitleDTO
{
[System.ComponentModel.DataAnnotations.Key]
public int ID { get; set; }

public string Title { get; set; }
}
}

Employee Domain Service

Here is part of the code from the EmployeeDomainService. I have added a method called GetTitles to return a list of all the titles and I have modified GetEmployees so that it now has HireDate and Title values being returned.

    [EnableClientAccess()]
[RequiresAuthentication()]
public class EmployeeDomainService : DomainService
{
UserDTO _user;

public EmployeeDomainService(UserDTO user)
{
_user = user;
}

public override void Initialize(DomainServiceContext context)
{
//load up custom configurations
base.Initialize(context);
}

public List<EmployeeDTO> GetEmployees() {
List<EmployeeDTO> employees = new List<EmployeeDTO>();

EmployeeDTO employee = new EmployeeDTO();
employee.ID = 1;
employee.FirstName = "Jason";
employee.LastName = "Apergis";
employee.Title = 1;
employee.HireDate = new DateTime(1990, 4, 15);
employees.Add(employee);

employee = new EmployeeDTO();
employee.ID = 2;
employee.FirstName = "Ethan";
employee.LastName = "Apergis";
employee.Title = 2;
employee.HireDate = new DateTime(2000, 6, 3);
employees.Add(employee);

employee = new EmployeeDTO();
employee.ID = 3;
employee.FirstName = "Caroline";
employee.LastName = "Apergis";
employee.Title = 3;
employee.HireDate = new DateTime(2005, 12, 20);
employees.Add(employee);

return employees;
}

public List<TitleDTO> GetTitles() {
List<TitleDTO> titles = new List<TitleDTO>();

TitleDTO title = new TitleDTO();
title.ID = 1;
title.Title = "Developer";
titles.Add(title);

title = new TitleDTO();
title.ID = 2;
title.Title = "Manager";
titles.Add(title);

title = new TitleDTO();
title.ID = 3;
title.Title = "Senior Developer";
titles.Add(title);

title = new TitleDTO();
title.ID = 4;
title.Title = "Senior Manager";
titles.Add(title);

return titles;
}

LookupProvider

I next created a class that has the responsibility of loading lookup values. The code is pretty basic in that when the Titles property is referenced, it will retrieve the appropriate list of values.

using System.Collections.Generic;
using System.ComponentModel;
using System.ServiceModel.DomainServices.Client;
using System.Linq;
using MVVM.Web.Model;
using MVVM.Web.Service;

namespace MVVM.ViewModel
{
public class LookupProvider : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private EmployeeDomainContext _context;

public LookupProvider() {
_context = new EmployeeDomainContext();
}

public List<TitleDTO> Titles {
get {
if (_context.TitleDTOs.Count == 0)
{
_context.Load <TitleDTO>(_context.GetTitlesQuery(), TitlesLoaded, false);
}

return _context.TitleDTOs.ToList();
}
}

private void TitlesLoaded(LoadOperation<TitleDTO> lo)
{
RaisePropertyChanged("Titles");
}

private void RaisePropertyChanged(string propertyname)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
}
}

}
}

TitleConverter

Next I created a class called TitleConverter which will be used for binding with the AutoCompleteBox. Note that it implements the IValueConverter interface. The Convert method take a numeric value passed in and retrieve the Title from the list. The ConvertBack does the opposite.

Note that if no values are found in the list, null is returned. This will actually happen a lot because these methods are called every time there is a keystroke in the control. Do not worry, it will not call out to the RIA service each time the user presses a key. It is just do a LINQ query against the list of Titles that is memory when the TitleConverter was first instantiated.

using System;
using System.Windows.Data;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using MVVM.Web.Model;

namespace MVVM.ViewModel
{
public class TitleConverter : IValueConverter
{
private LookupProvider _lookupProvider;

public TitleConverter() {
_lookupProvider = new LookupProvider();
}

public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {

if (value == null)
{
return null;
}

int i;

try
{
i = System.Convert.ToInt32(value);
}
catch (Exception ex)
{
return null;
}

IEnumerable<string> titles =
from t in _lookupProvider.Titles
where i == t.ID
select t.Title;

if (titles.Count() > 0)
{
return titles.FirstOrDefault();
}

return null;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {

if (value == null)
{
return null;
}

string title;

try
{
title = System.Convert.ToString(value);
}
catch (Exception ex)
{
return null;
}

IEnumerable<int> titles =
from t in _lookupProvider.Titles
where title == t.Title
select t.ID;

if (titles.Count() > 0)
{
return titles.FirstOrDefault();
}

return null;
}
}
}

Binding to User Controls Outside of Grid

So first let’s look to see how things are bound outside of the grid. As I mentioned, I added to my user control a DatePicker, ComboBox and AutoCompleteBox.

The DatePicker is really straight forward; all we need to do is bind on the SelectedDate property in the same way we have bound other controls.

Next the ComboBox needs to be configured. We need to show the list of titles in the dropdown and bind it to the Title property of the current EmployeeDTO object that is in the EmployeeViewModel object instance. To set the values in the dropdown, we need to first set the ItemSource attribute. The ItemSource is bound to the Titles property of the LookupProvider which returns a List<TitleDTO>. Notice there is a UserControl.Resources tag at the top which creates an instance of the LookupProvider. Then both the DisplayMemberPath and SelectedValuePath are set to the appropriate properties of the TitleDTO class. Finally the Combo Box has the SelectedValue attribute which needs to be set to the Title property of the EmployeeDTO class. Now when a Title is selected in the dropdown the integer id for the TitleDTO will be set to the EmployeeDTO object.

The AutoCompleteBox is similar but there are a few differences in the configuration. The ItemSources attribute is again set to Titles property of the LookupProvider. Next the ValueMemberBinding is set to Title of the TitleDTO. As well the TextBlock is bound to the Title of the TitleDTO (the TextBlock is the control that is displayed when the user has not clicked into the AutoCompleteBox). Now we need to bind the actual Employee object to the Text attribute. Notice the Converter is set to StaticResrouce titleConverter. This is where we have to use the TitleConverter class that I created earlier. I had to also add a UserControl.Resources tag to create an instance of the TitleConverter.

<UserControl x:Class="MVVM.View.UpdateView"
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:mvvm="clr-namespace:MVVM.ViewModel"
mc:Ignorable="d"
d:DesignHeight="79" d:DesignWidth="642" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">

<UserControl.Resources>
<mvvm:LookupProvider x:Name="lookupProvider" />
<mvvm:TitleConverter x:Key="titleConverter" />
</UserControl.Resources>

<Grid x:Name="LayoutRoot" Background="White">
<TextBox Height="23" HorizontalAlignment="Left" Margin="12,12,0,0"
Name="txtFirstName" VerticalAlignment="Top" Width="120"
Text="{Binding SelectedEmployee.FirstName, Mode=TwoWay}"/>
<TextBox Height="23" HorizontalAlignment="Left" Margin="138,12,0,0"
Name="txtLastName" VerticalAlignment="Top" Width="120"
Text="{Binding SelectedEmployee.LastName, Mode=TwoWay}"/>
<Button Content="Save" Height="23" HorizontalAlignment="Left"
Margin="559,46,0,0" Name="btnSave" VerticalAlignment="Top"
Width="75" Command="{Binding SaveCommand}"/>
<sdk:DatePicker Height="23" HorizontalAlignment="Left"
Margin="262,12,0,0" Name="dtpHireDate"
VerticalAlignment="Top" Width="120"
SelectedDate="{Binding SelectedEmployee.HireDate, Mode=TwoWay}"/>
<ComboBox Height="23" HorizontalAlignment="Left" Margin="388,12,0,0"
Name="cboTitle" VerticalAlignment="Top" Width="120"
ItemsSource="{Binding Titles, Source={StaticResource lookupProvider}}"
DisplayMemberPath="Title"
SelectedValuePath="ID"
SelectedValue="{Binding SelectedEmployee.Title, Mode=TwoWay}"/>
<sdk:AutoCompleteBox Height="28" HorizontalAlignment="Left"
Margin="514,12,0,0" Name="acbTitle"
VerticalAlignment="Top" Width="120"
ItemsSource="{Binding Titles, Source={StaticResource lookupProvider}}"
ValueMemberBinding="{Binding Title}"
Text="{Binding SelectedEmployee.Title, Mode=TwoWay, Converter={StaticResource titleConverter}}">
<sdk:AutoCompleteBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</sdk:AutoCompleteBox.ItemTemplate>
</sdk:AutoCompleteBox>
</Grid>
</UserControl>

Binding to User Controls in a Grid

The last part of this blog I am going to discuss is how to set up these same controls in a grid. First I set the AutoGenerateColumns attribute of the grid to false. Then I have to start manually adding columns into the <sdk:DataGrid.Columns>. Notice setting up textboxes is really straight forward.

Next I need to set up the DatePicker control. I first add a DataGridTemplateColumn which will allow me to basically add any control that I want to into a grid column. In this case I put in a DataPicker and the binding of that column is identical to above.

Next I need to put in the ComboBox and the AutoCompleteBox in the same manner reusing the same xaml code I used above.

<UserControl x:Class="MVVM.View.EmployeeView"
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:mvvm="clr-namespace:MVVM.ViewModel"
mc:Ignorable="d"
d:DesignHeight="210" d:DesignWidth="645" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">

<UserControl.Resources>
<mvvm:LookupProvider x:Name="lookupProvider" />
<mvvm:TitleConverter x:Key="titleConverter" />
</UserControl.Resources>

<Grid x:Name="LayoutRoot" Background="White">
<sdk:DataGrid AutoGenerateColumns="False" Name="grdEmployees"
Height="168" VerticalAlignment="Top"
HorizontalAlignment="Left" Width="645"
ItemsSource="{Binding Employees}"
SelectedItem="{Binding SelectedEmployee, Mode=TwoWay}">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn
Header="ID" Binding="{Binding ID}" />
<sdk:DataGridTextColumn
Header="First Name" Binding="{Binding FirstName}" />
<sdk:DataGridTextColumn
Header="Last Name" Binding="{Binding LastName}" />
<sdk:DataGridTemplateColumn Header="Hire Date">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<sdk:DatePicker
SelectedDate="{Binding HireDate, Mode=TwoWay}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
<sdk:DataGridTemplateColumn Header="Title">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Titles, Source={StaticResource lookupProvider}}"
DisplayMemberPath="Title"
SelectedValuePath="ID"
SelectedValue="{Binding Title, Mode=TwoWay}"/>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
<sdk:DataGridTemplateColumn Header="Auto Title">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<sdk:AutoCompleteBox Name="acbTitle"
IsTextCompletionEnabled="True"
ItemsSource="{Binding Titles, Source={StaticResource lookupProvider}}"
ValueMemberBinding="{Binding Title}"
Text="{Binding Title, Mode=TwoWay, Converter={StaticResource titleConverter}}">
<sdk:AutoCompleteBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</sdk:AutoCompleteBox.ItemTemplate>
</sdk:AutoCompleteBox>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
<Button Content="Insert" Height="23" HorizontalAlignment="Left" Margin="479,175,0,0"
Name="btnInsert" VerticalAlignment="Top" Width="75"
Command="{Binding InsertCommand}"/>
<Button Content="Delete" Height="23" HorizontalAlignment="Left" Margin="560,175,0,0"
Name="btnDelete" VerticalAlignment="Top" Width="75"
Command="{Binding DeleteCommand}"/>
</Grid>
</UserControl>

Gird Configuration Alternative

There is one small alternative you can do with the display of controls in a grid. If the user has no clicked into a cell, you may only want to show a text control but when they click into the cell to edit the value, show them the appropriate control. For instance you may want to only show the DatePicker or ComboxBox when the user has clicked in to save on real estate. Only downside of this approach is the user may have to do a little more clicking.

Here is how you would do this for the DatePicker column that I showed you above. Here you will see there is a CellTemplate and a CellEditingTemplate.

<sdk:DataGridTemplateColumn Header="Hire Date">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding HireDate}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
<sdk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<sdk:DatePicker SelectedDate="{Binding HireDate, Mode=TwoWay}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellEditingTemplate>
</sdk:DataGridTemplateColumn>

5 comments:

Unknown said...

Thank you so much, realy thats exactly i need, but i have some problem with this example
I have a problem with performance, i am replace the code in EmployeeDomainService just get the data from database, the problem foucs about GetTitles function, this function has been connection many times to get the data from database becouse i put Titles in the Datagrid. what can i do?
look i need to call EmployeeDomainService just one time, and i need a method to build relationship between Employees and Titles in ViewModel, again thank you for your good articles.

Jason Apergis said...

Eyad,

I do not have any handy code samples but I did have to solve this problem because I too had lots of lookups and we need to reduce the load and improve performance. Please read about the Singleton pattern. Just search on ".NET Singleton".

What you will basically do is load up a Context object within your singleton. It will only be loaded one time. Then you can reference that singleton with your loaded lookup data in all of your screens.

Jason

Unknown said...

Thank you, it helped a lot.

Abhilash said...

Thank you very much,it is very helpful to me.
But I have some problem with the filling of the data from database using RIA.
In my database in the place of Titles there is more than 2400 records.
when I select one employee then his title will not display because TitleConverter function will execute before filling the _lookupProvider.Titles values.

Jason Apergis said...

You should look at the paging feature of the Silverlight grid. 2400 items is a lot for user to look at and probably not going to be usable.