Tuesday, February 9, 2010

Using a ComboBox on a DataForm for Silverlight 3

Disclaimer: I think my next solution is better. Give it a try. Click here to check it out.

I love the DataForm for Silverlight 3. It is truly awesome. My biggest complaint is that customizing the fields has changed significantly over time. For instance it now longer supports Fields, which I thought worked well.

With that said, the DataForm for Silverlight 3 has the ability to automatically generate the UI based on the objects which you pass it. The collection you pass it can even have different types of objects. This functionality is very useful for doing things that are textfields, calendars, numbers, etc.

The other option is to use templates if you can’t leave it up to the DataForm to decide what to display based on what you pass it. The problem is your UI is now tied on a field by field basis to what object you pass it. While that is not usually an issue, it is a bummer to lose that cool functionality.

The problem comes when you want to add a combo box to the DataForm. All of a sudden it seems that support is thrown out the window. I thought ASP.NET Dynamic Data did a pretty good job of handling DropDownLists, but I am disappointed with how the DataForm for Silverlight handles it. It seems that I have to work too hard just to implement a ComboBox.

The good news is that I did manage to figure out how to use a ComboBox with not much code at all; at least once you have created an edit template for your DataForm. Another bit of good news is that the DataForm makes it very easy to create an edit template. The sample below shows what an edit template looks like. One interesting thing is that since I did not specify any other templates, they are automatically generated it appears. Very nice of the DataForm. :)

Here is the XAML that defines a DataForm that has one hidden ID field, 3 textfields, and one drop down to select the Owner from a list of People.

<dataFormToolkit:DataForm x:Name="dfCar" AutoGenerateFields="True" >
<dataFormToolkit:DataForm.EditTemplate>
<DataTemplate>
<StackPanel>
<dataFormToolkit:DataField Visibility="Collapsed">
<TextBox Text="{Binding CarID, Mode=TwoWay}" />
</dataFormToolkit:DataField>

<dataFormToolkit:DataField>
<TextBox Text="{Binding Make, Mode=TwoWay}" />
</dataFormToolkit:DataField>

<dataFormToolkit:DataField>
<TextBox Text="{Binding Model, Mode=TwoWay}" />
</dataFormToolkit:DataField>

<dataFormToolkit:DataField>
<TextBox Text="{Binding Color, Mode=TwoWay}" />
</dataFormToolkit:DataField>

<dataFormToolkit:DataField>
<ComboBox x:Name="cboOwner" DisplayMemberPath="Name" DataContext="{Binding OwnerID}"/>
</dataFormToolkit:DataField>
</StackPanel>
</DataTemplate>
</dataFormToolkit:DataForm.EditTemplate>
</dataFormToolkit:DataForm>



In the constructor of user control that contains this DataForm we need to register for the ContentLoaded event of the DataForm. This will be called after the Template is loaded and thus the ComboBox (cboOwner) has been created. Be sure NOT to register for the Loaded event by mistake. This is too early in the lifecycle of the DataForm and the ComboBox will not have been created by then. You must wait for the ContentLoaded event to fire before looking for the ComboBox.




void dfCar_ContentLoaded(object sender, DataFormContentLoadEventArgs e)
{
// populate the ComboBox
ComboBox cbo = dfCar.FindNameInContent("cboOwner") as ComboBox;
cbo.ItemsSource = GetOwnersList();

// Select the appropriate item in the ComboBox based on the ID
int? personID = (dfCar.CurrentItem as Car).OwnerID;
if (personID.HasValue)
{
Person foundPerson = (cbo.ItemsSource as ObservableCollection<Person>).First(p => p.ID == personID.Value);
cbo.SelectedItem = foundPerson;
}
}

// some supporting stuff just so you can see what they look like.
// Your objects and methods will likely be more complex, and pull data from a web services,
// RIA Services, WCF, etc.
public class Person
{
public string Name { get; set; }
public int ID { get; set; }
}

public List<Person> GetOwnersList()
{
List<Person> people = new List<Person>();
people.Add(new Person { Name = "Brent", ID = 0 });
people.Add(new Person { Name = "Amanda", ID = 1 });
people.Add(new Person { Name = "Lance", ID = 2 });
return people;}


As you can see, most of this isn’t extra code so much as it is just stuff that would need regardless. The ContentLoaded event handler is really all that we had to add that was extra and that code is pretty straight forward.



I really hope I am missing something. It seems to me that I should not have to do this, and I also don’t think I should have to search the ComboBox by ID. It seems that the ASP.NET model (DataMember property to specify what property in the object should be used as a “value” matching property) would be nice here.



If anyone knows of a better way I am very interested to hear about it.



I got the idea from here.




11 comments:

Jan D'Hondt said...

Hello,
I've just read your article and applied it to my dataform. The problem I have now is the following:
The dataform has an edit button (little pencil) to go into edit mode. 2 buttons appear OK and Cancel. The OK button is disabled until you go and change some data in the dataform. But when I change the selection in the combobox the OK button remains disabled. And I am stuck again.

Brent V said...

Hi Jan,

I did figure that out since I posted. I updated the post. Try adding something like DataContext="{Binding OwnerID}" to the ComboBox tag. That should do it. Let me know if that works for you also.

Thanks,

Brent

Brent V said...

Jan,

Also, if you want to go about it a different way, I understand you can write a Converter to convert between object and id or use Data Services may help also. I have not tried either, but that is what I read I think. I have written converters for a DataGrid and they are not difficult to write. They are actually quite simple. It seems silly that I should have to write them, but they are at least easy.

Brent

Jan D'Hondt said...

Brent
Thank you for the response. I will try your first suggestion asap. I'll let you know the result. I had read and tried the convertors before I found your blog. I prefer to keep it as simple as possible, that is why I tried your approach.

Jan D'Hondt said...

The solution with DataContext="{Binding OwnerID}" did not work. Scrolling from record to record with the navigation buttons of the dataform work, the combobox displaymemeber is updated nicely. But when I click the Edit button and select another item from the combobox, the OK button remains disabled. I can only click the Cancel button, or modify some text in a textbox, then the OK button is available to save the changes.

Brent V said...

Hi Jan,

I think you might be interested in checking out my other solution to getting the ComboBox to work on a DataForm. Here is the url: http://justgeeks.blogspot.com/2010/03/better-way-of-using-combobox-on.html

I hope this helps.

Brent

Jan D'Hondt said...

Hi Brent,

Thank you for the example. One last thing: the OK button becomes enabled when you change the country. But the cancel button does not.

Brent V said...

Hi Jan,

That is because your entities need to implement IEditableObject. As far as I know, that is a requirement for the Cancel button. In particular, you will need to handle the cancel event. There are a couple of ways to do this. One is just reload the record from the database (for an existing record) and blank the values for new. Another way, is on BeginEdit event you make a copy of the entity, then on cancel, you copy the values of that object back to the current one. If you don't implement the cancel event, the cancel button will enable and disable correctly, but it won't undo changes to form.

I hope that helps.

Brent

Jonas Fagerberg said...

To enable the buttons in the DataForm when an item is selected in the ComboBox you can use the CurrrentItem property of the DataForm and set the underlying value in the ObservableCollection. Use the DropDownClosed event handler to acheive the goal.

The first row in the event handler gets the values from the ComboBox, and the second row sets the value in the underlying collection.


private void cboPersonType_DropDownClosed(object sender, EventArgs e)
{
int personTypeID = (int)((PersonType)((ComboBox)sender).SelectionBoxItem).PersonTypeID;
(frmPerson.CurrentItem as Persons).PersonTypeID = personTypeID;
}

JRod said...

For my situation I needed to go with Silverlight 3 and implementing a combobox has been horrendous. I just want to thank you for this solution. I have suffered through many other attempted solutions and this one saved my sanity and perhaps my marriage :). I did look at your second post, but I needed this particular approach for my particular problem. So again, thank you.

Robert said...

Sencillo y claro ejemplo.. :-)
Gracias, me sirvio muchisimo!

Roberto