Enumeration Binding in Silverlight

by Dean 31. January 2009 10:57

A contractor colleague of mine (Phil Steel) had an interesting Silverlight problem yesterday. He wanted to populate a Silverlight ComboBox with an enumeration, and implement 2-way binding to a property on his Data class (i.e. binding his property to the ‘SelectedItem’ on the ComboBox.

His requirements were as follows:

  1. The solution must have full design-time support in Expression Blend
  2. The code must give the developer an opportunity to create metadata for the enumeration that can be used for ‘user friendly’ visual values in the ComboBox
  3. There must be full 2-way binding, so no need to tap into the ‘SelectionChanged’ event to update the data object.
  4. Minimal C# code, with as much as possible being re-useable ‘as is’ for any enumeration.

Ok, so I googled around and only found fragments of a possible solution, so I decided to engineer one myself.

The details are below

Task 1 : Create reusable code that converts a .NET enumeration into a collection

 

There are 2 classes we need as part of this task, and the code is below

public sealed class EnumContainer
{
    public int EnumValue { get; set; }
    public string EnumDescription { get; set; }
    public object EnumOriginalValue { get; set; }
    public override string ToString()
    {
        return EnumDescription;
    }
 
    public override bool Equals(object obj)
    {
        if (obj == null)
            return false;
        return EnumValue.Equals((int)obj);
    }
 
    public override int GetHashCode()
    {
        return EnumValue.GetHashCode();
    }
 
}
 
public class EnumCollection<T> : List<EnumContainer> where T : struct 
{
    public EnumCollection()
    {
        var type = typeof (T);
        if (!type.IsEnum)
            throw new ArgumentException("This class only supports Enum types");
        var fields = typeof (T).GetFields(BindingFlags.Static | BindingFlags.Public);
        foreach(var field in fields)
        {
            var container = new EnumContainer();
            container.EnumOriginalValue = field.GetValue(null);
            container.EnumValue = (int) field.GetValue(null);
            container.EnumDescription = field.Name;
            var atts = field.GetCustomAttributes(false);
            foreach (var att in atts)
                if (att is DescriptionAttribute)
                {
                    container.EnumDescription = ((DescriptionAttribute) att).Description;
                    break;
                }
            Add(container);
        }
        
    }
}

 

These 2 classes represent a bindable version of our enumeration. One class is an object that represents the enumeration values (EnumContainer), and the other is the collection.

(The part of the code above describing ‘CustomAttributes’ relates to the task below.

 

Task 2 (optional) : Add ‘user friendly’ metadata to your enumerations

 

This is a well known solution using the ‘DescriptionAttribute’ class

 

    public enum CustomerStatus
    {
        [Description("Not Yet Approved")]
        UnApproved,
 
        [Description("Pending Approval")]
        PendingApproval,
 
        [Description("Fully Approved")]
        Approved
    }

These descriptions will appear in your combobox

 

Task 3 : Create an IValueConverter to support 2-way binding of the ComboBox to the data object.

 

public class EnumValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, 
                CultureInfo culture)
    {
        return (int) value;
    }
 
    public object ConvertBack(object value, Type targetType, object parameter, 
CultureInfo culture)
    {
        if (value == null)
            return null;
        if (value.GetType() == targetType)
            return value;
        return ((EnumContainer) value).EnumOriginalValue;
    }
}

 

Task 4 : Implement a solution to suit your needs

 

Ok, now that we have all the base code added to our project, it’s time to implement a solution to our problem. The only c# class that needs to be created is a simple collection class that inherits from EnumCollection and allows full designer support in Expression Blend

So here (for example) are our data classes that we will use to demonstrate our solution

1) Our example ‘Customer’ class

public class Customer
{
    public string CustomerName { get; set; }
    public CustomerStatus Status { get; set; }
}

2) Our example Enumeration (notice the use of the DescriptionAttribute, which gives the ComboBox user friendly test)

 

    public enum CustomerStatus
    {
        [Description("Not Yet Approced")]
        UnApproved,
 
        [Description("Pending Approval")]
        PendingApproval,
 
        [Description("Fully Approved")]
        Approved
    }

 

3) Our collection to provide Blend support (as you can see, its just a simple inheritor as XAML doesnt easily support generics).

public class CustomerStatusEnumeration : EnumCollection<CustomerStatus>
{
 
}

 

4) Here's an example of wiring up the data object in the code

 

private void PageLoaded(object sender, RoutedEventArgs e)
{
    customer = new Customer() { CustomerName = "Customer1", 
        Status = CustomerStatus.PendingApproval };
    DataContext = customer;
}

 

Task 5 : Wire it all up in the Xaml

<UserControl x:Class="TestEnum.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300" xmlns:TestEnum="clr-namespace:TestEnum" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d">
    <UserControl.Resources>
        <TestEnum:CustomerStatusEnumeration 
            x:Key="CustomerStatusEnumerationDS" 
            d:IsDataSource="True"/>
        <TestEnum:EnumValueConverter  
            x:Key="EnumConverter" />
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <TextBox Text="{Binding Mode=TwoWay, Path=CustomerName}" 
                 Margin="5" HorizontalAlignment="Left" VerticalAlignment="Top" />
        <ComboBox Name="Combo" HorizontalAlignment="Left" 
                  VerticalAlignment="Top" 
                  ItemsSource="{Binding Mode=OneWay, 
                        Source={StaticResource CustomerStatusEnumerationDS}}" 
                  SelectedItem="{Binding Status, Mode=TwoWay, 
                        Converter={StaticResource EnumConverter}}" 
                  Margin="5" Grid.Column="1"/>
        <Button Content="Update" Click="UpdateCustomer" Margin="5" 
                HorizontalAlignment="Left" 
                VerticalAlignment="Top" Grid.Column="2" />
    </Grid>
</UserControl>

An here’s a screenshot of the above solution

enumexample

As you can see, it’s a very simple and effective solution. And should we require to wire up a second enum to our Silverlight app then the only class we’d need to create (assuming the enum already exists) is the empty collection class as described above.

 

I hope you like the solution, any comments about how to improve it then please let me know.

 

Dean

Tags: , ,

WCF | Silverlight | DataBinding

Fixed - Issues With WCF and Mixed VS/Blend Development

by Dean 25. January 2009 12:37

Some of you may have come across an issue when developing ‘fast and dirty’ demo apps in Silverlight that have a WCF backend service on the web application.

When developing throwaway demo apps for clients, you need to take all of the shortcuts you can get, so I always use the ‘Add service reference’ feature of Visual Studio to add a service reference within my Silverlight app to the host ASP.NET service (not advisable for production-quality apps though). This is a great feature because as you change the service interface you can keep in sync on your Silverlight app using a single service ‘Update’ button.

However, the big drawback of this approach is that the Visual Studio tool that creates the service reference hard-codes the service Uri into the generated classes.

If you were just using Visual Studio then this wouldn't be a problem, but unfortunately VS and Blend use different development servers that cannot co-exist on the same TCP port, so for one of the two development environments the Uri of the service is going to be wrong.

for example, you create the service reference in Visual Studio, which is configured to run the website on url ‘http://localhost:43667/DemoApp’ , then the hard-coded Uri for the service will be something like ‘http://localhost:43667/DemoApp/Service1.svc’.

However, Expression Blend will always run the solution on a different port, so it will run the app on a base url of something like ‘http://localhost:52234/DemoApp/’ which means that the Silverlight app wont access the service and your app will not be able to access the service when run from Blend.

Your gut reaction might be to try the following:

  1. Fix the port in Visual Studio to be the same as the one in Blend. – This wont work because Blend will detect VS is on it’s preferred port and will automatically switch to an alternative port.
  2. Use your local version of IIS to avoid the clash of the ports. – This will work but doesn’t really fit in with the idea of creating a portable throwaway demo app.
  3. Work in both VS and Blend, but only press F5 while in VS. This works but is a pain in the butt.
  4. Play with crossdomain policy files etc. – Not really a solution, especially as VS always tears-down the dev server when in DEBUG mode.

However, there is a solution for this scenario that only requires changing a single line of code to your Silverlight project.

Below is the some ‘Before’ sample code

private void PageLoaded(object sender, RoutedEventArgs e)
{
    var service = new Service1Client();
    service.DoWorkCompleted += (source, args) => MessageBox.Show("DONE");
    service.DoWorkAsync();
}


And now here is the same code with an added line that fixes this issue.

 

private void PageLoaded(object sender, RoutedEventArgs e)
{
    var service = new Service1Client(new BasicHttpBinding(BasicHttpSecurityMode.None), 
        new EndpointAddress(Application.Current.Host.Source.AbsoluteUri.Replace(
            Application.Current.Host.Source.AbsolutePath,"/Service1.svc")));
    service.DoWorkCompleted += (source, args) => MessageBox.Show("DONE");
    service.DoWorkAsync();
}

 

Basically, we’ve just changed the way we’ve constructed the service object, and passed in the necessary parameters to make the service reference relative rather than absolute. (You may need to replace the ‘Service1.svc’ parameter with one that is relevant to your project).

This is a neat little code snippet to keep on hand when developing these kinds of apps

I hope this helps you fix this annoying issue in your Silverlight development.

If you have any further suggestions then let me know.

 

Dean

Tags: , ,

Silverlight | DataBinding | WCF

Most comments

Tom Tom
1 comments
Derek Lakin Derek Lakin
1 comments
gb United Kingdom
Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2010 ButtonChrome.com