As promised here’s the way to use RIA entities as ItemSource on DataFormComboBoxFields. The first things we need to keep in mind is that all RIA calls are asynchronous and that once the ItemSource is set, ComboBoxFields do not automatically refresh their itemlists. This means that we can only set the ItemSource once our RIA call has finished getting the items we want in the list.
The first thing to do is call a Load method on the DomainContext to get the Entities we want in the combo and subscribe to the Loaded event of the context so we know when to set the ItemSource property on the combo.
public partial class CustomerPage : Page
{
protected CustomerDomainContext customerDomainContext;
public CustomerPage()
{
InitializeComponent();
customerDomainContext = new CustomerDomainContext();
this.Loaded += new RoutedEventHandler(Page_Loaded);
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
customerDomainContext.Loaded += DomainContext_Loaded;
customerDomainContext.LoadCustomers();
}
protected virtual void DomainContext_Loaded(object sender, System.Windows.Ria.Data.LoadedDataEventArgs e)
{
if (customerDomainContext.Customers.Count > 0)
{
DatabindComboBoxes();
}
}
protected virtual void DatabindComboBoxes()
{
DataFormBinding.BindEntity2DataForm<Customer>(orderDataForm, "StringCustomerID", customerDomainContext.Customers);
}
...
}
The next step is to actually set the ItemSource on the ComboBoxField. The problem here is that we need to have a readable item in the list which translates to the foreign key in the entity databound to the DataForm control. The way we do this is by using the Binding, DisplayMemberPath and a ValueConverter. The ValueConverter will be set later in code (in the DataBindComboBoxes method), but we’ll set the Binding and DisplayMemberPath in XAML.
...
<dataControls:DataForm x:Name="orderDataForm" Header="Order" AutoGenerateFields="False" CurrentItem="{Binding ElementName=ordersGrid, Path=SelectedItem}">
<dataControls:DataForm.Fields>
...
<dataControls:DataFormComboBoxField FieldLabelContent="Customer" DisplayMemberPath="CustomerName" Binding="{Binding StringCustomerID, Mode=TwoWay}" x:Name="cboCustomer"/>
...
dataControls:DataForm.Fields>
dataControls:DataForm>
...
Now, unfortunately there is a bug in the current beta version of Silverlight 3. The effect of this bug is that although I’ve set an x:Name on the ComboBoxField, it will not be exposed in code as a dependency property. There is a nasty workaround that uses the Binding property to resolve the field. The code to do this below I found in large part at
here at Bob Baker’s blog at MicroApplications.com, but I’ve made it more generic so I could use any entity:
public static class DataFormBinding
{
public static DataFormBoundField GetFieldByBindingPath(DataForm dataForm, string bindingPath)
{
Stack<DataFormField> fieldStack = new Stack<DataFormField>();
foreach (DataFormField field in dataForm.Fields)
{
fieldStack.Push(field);
}
while (fieldStack.Count > 0)
{
DataFormField curField = fieldStack.Pop();
DataFormBoundField boundField = curField as DataFormBoundField;
if (boundField != null &&
boundField.Binding != null &&
boundField.Binding.Path != null &&
boundField.Binding.Path.Path == bindingPath)
{
return boundField;
}
else
{
DataFormFieldGroup fieldGroup = curField as DataFormFieldGroup;
if (fieldGroup != null)
{
foreach (DataFormField field in fieldGroup.Fields)
{
fieldStack.Push(field);
}
}
}
}
return null;
}
public static EntityListValueConvertor BindEntity2DataForm(DataForm dataForm, string linkProperty, EntityList entityList) where T : Entity, new()
{
return BindEntity2DataForm(dataForm, linkProperty, linkProperty, entityList);
}
public static EntityListValueConvertor BindEntity2DataForm(DataForm dataForm, string parentProperty, string childProperty, EntityList entityList) where T : Entity, new()
{
if (entityList.Count > 0)
{
DataFormComboBoxField cbo = DataFormBinding.GetFieldByBindingPath(dataForm, childProperty) as DataFormComboBoxField;
if (cbo != null && cbo.ItemsSource == null)
{
EntityListValueConvertor converter
= new EntityListValueConvertor((EntityList)entityList, parentProperty);
cbo.Binding =
new Binding(childProperty)
{
ElementName = cbo.Binding.ElementName,
Mode = BindingMode.TwoWay,
Converter = converter,
UpdateSourceTrigger = UpdateSourceTrigger.Explicit,
ValidatesOnExceptions = true
};
cbo.ItemsSource = entityList;
return entityList;
}
}
return null;
}
}
The BindEntity2DataForm methods will allow me to create a new ValueConverter and set the ItemSource and Binding properties in one go. There are 2 overloads just in case the property name on the parent entity is not the same as the one on the child entity. The next block of code contains the ValueConverter itself. I’ve made a realy generic one so I can use any IEnumerable, the GenericValueConverter, but in this case I need it only to convert RIA Entities for me, so I us it’s EntityListValueConverter variant.
public class EntityListValueConvertor : GenericValueConverterEntityList> where T : Entity, new()
{
public EntityListValueConvertor(EntityList entityList, string linkProperty)
: base(entityList, linkProperty)
{
}
}
public class GenericValueConverter : IValueConverter where L : IEnumerable
{
public GenericValueConverter(L list, string linkProperty)
{
this.List = list;
this.LinkProperty = linkProperty;
}
public L List { get; set; }
protected PropertyInfo linkPropertyInfo;
public string LinkProperty
{
get
{
return linkPropertyInfo != null ? linkPropertyInfo.Name : string.Empty;
}
set
{
linkPropertyInfo = string.IsNullOrEmpty(value) ? null : typeof(T).GetProperty(value);
}
}
private object GetValueByLinkProperty(T item)
{
return (linkPropertyInfo != null) ? linkPropertyInfo.GetValue(item, null) : item ;
}
public T Convert(object value)
{
if (List != null && value != null && value is IComparable)
{
foreach (T item in this.List)
{
if (((IComparable)value).Equals(GetValueByLinkProperty(item)))
{
return item;
}
}
}
return default(T);
}
public object ConvertBack(T value)
{
T item = (T)value;
if (item != null)
{
return GetValueByLinkProperty(item);
}
return null;
}
#region IValueConverter Members
object IValueConverter.Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return Convert(value);
}
object IValueConverter.ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (value is T) ? ConvertBack((T)value) : null;
}
#endregion
}
Is that it? Well, normally it would be, but there is another bug you will need to take into account if the property you are trying to bind is a System.Guid: It turns out that when a ComboBoxField is bound to a Guid the combobox will always be disabled, although the right bound value is shown in the grayed box, you cannot change it. With any other type of field as foreign key, it works fine.
I had a lot of fun finding that one, I can tell you.
And I wasn’t the only one. Anyway here’s the workaround for this problem: you can define an additional property in a partial class (in the Silverlight project if you're using RIA Services) to accompany your Entity.
For the parent entity this could look something like this:
public sealed partial class Customer
{
public string StringCustomerID
{
get { returnthis.CustomerID!=null?this.CustomerID.ToString():null; }
}
}
For the child entity you will need to add a setter as well:
public sealed partial class Order
{
public string StringCustomerID
{
get { returnthis.CustomerID!=null?this.CustomerID.ToString():null; }
set { this.CustomerID=value!=null?newGuid(value):Guid.Empty; }
}
}
I can tell that I spent a lot of time on this, you would have thought, pretty basic feature. I hope it will be better supported in the release, but for now, you will need to create the helper methods like I have done so you can at least manage to accomplish this with just calling a method like my BindEntity2DataForm when the Entities have finished loading.
By Nick Verschueren, .Net Solutions Architect