Guidance for combobox in Silverlight datagrid using Prism

Topics: Prism v2 - Silverlight 3
Feb 25, 2010 at 12:42 AM
I've run into what I believe to be a general Silverlight 2 limitation and was hoping to get MVVM/Prism guidance on how to overcome it. I'll give a brief description of the problem and provide the relevant code snippets.

Scenario:

I'm using Prism and the MVVM pattern on a project which has a DataGrid and I'd like to use a ComboBox in the CellEditingTemplate to allow the end user to select a new value from a list. To enable this scenario, I've exposed two properties on my ViewModel - Types and Items. Types is an array of a class containing properties Key and Value and is where I store my display text and actual value for the ComboBox. Items is an ObservableCollection of my model class which exposes a property which is the type of my Key/Value class.

The project is setup such that I have a ContainerControlledLifetimeManager Service which returns values to a ViewModel which is bound to by the View: (i.e. Service <--> ViewModel <--> View).

Problem:

When I set the ItemsSource of the DataGrid like so, ItemsSource={Binding Path=Items}, the ComboBox contained within the CellEditingTemplate is unable to bind to the Types property of my ViewModel because the relative binding path now has Items as the root.

Solution?:

The obvious solution would be to reset the root of the relative binding path back to the DataContext for the ComboBox (something like ItemsSource={Binding Path=Types, Source={DataContext}}, but I've searched high and low and haven't found a way to do that. All the other solutions I've seen involves setting up a class that exposes the property you want to bind to as a StaticReource. However, in my scenario that won't work (at least I don't think it will) because I'm using the ContainerControlledLifetimeManager Service to get the values for the Types property and as far as I know any class you markup as a resource needs to have an empty default constructor - which would prevent me from being able to pull the service from the container using constructor Dependency Injection.

For the time being, I've handled the binding in the code behind in the PreparingCellForEdit event handler, but I would definitly prefer a pure XAML/binding method if one exists.

The main code to reconstruct the scenario is below. Thanks in advance for any guidance you can provide.

Ryan

Module

using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;
using SLPrismGridComboBox.ModuleA.Services;
using SLPrismGridComboBox.ModuleA.ViewModels;
using SLPrismGridComboBox.ModuleA.Views;

namespace SLPrismGridComboBox.ModuleA
{
    public class ModuleAModule : IModule
    {

        IUnityContainer _container;
        IRegionManager _regionManager;

        public ModuleAModule(IUnityContainer container, IRegionManager regionManager)
        {
            _container = container;
            _regionManager = regionManager;
        }

        public void Initialize()
        {
            RegisterTypes();

            IMiscView view = _container.Resolve<IMiscView>();
            _regionManager.AddToRegion("MainRegion", view);
        }

        private void RegisterTypes()
        {
            _container.RegisterType<IMiscView, MiscView>();
            _container.RegisterType<IMiscViewModel, MiscViewModel>();
            _container.RegisterType<IMiscService, MiscService>(new ContainerControlledLifetimeManager());
        }

    }
}

Models
namespace SLPrismGridComboBox.ModuleA.Models
{
    public class SimpleKeyValuePair
    {
        public string Key { get; set; }
        public string Value { get; set; }

        public SimpleKeyValuePair(string key, string value)
        {
            this.Key = key;
            this.Value = value;
        }

        public override bool Equals(object obj)
        {
            if (obj.GetType().Equals(this.GetType()))
            {
                if (((SimpleKeyValuePair)obj).Value.Equals(this.Value))
                {
                    return true;
                }
            }
            return false;
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }
}

namespace SLPrismGridComboBox.ModuleA.Models
{
    public class MiscModel
    {

        public string Name { get; set; }
        public SimpleKeyValuePair Type { get; set; }

    }
}

Service
using System.Collections.ObjectModel;
using SLPrismGridComboBox.ModuleA.Models;

namespace SLPrismGridComboBox.ModuleA.Services
{
    public interface IMiscService
    {
        ObservableCollection<MiscModel> GetAllMiscModels();
        SimpleKeyValuePair[] GetAllTypes();
    }
}
using System.Collections.Generic;
using System.Collections.ObjectModel;
using SLPrismGridComboBox.ModuleA.Models;

namespace SLPrismGridComboBox.ModuleA.Services
{
    public class MiscService : IMiscService
    {
        public ObservableCollection<MiscModel> GetAllMiscModels()
        {
            ObservableCollection<MiscModel> result = new ObservableCollection<MiscModel>();
            SimpleKeyValuePair[] types = GetAllTypes();
            int i = -1;

            result.Add(new MiscModel() { Name = "Name 1", Type = types[i += 1] });
            result.Add(new MiscModel() { Name = "Name 2", Type = types[i += 1] });
            result.Add(new MiscModel() { Name = "Name 3", Type = types[i += 1] });
            result.Add(new MiscModel() { Name = "Name 4", Type = types[i += 1] });
            result.Add(new MiscModel() { Name = "Name 5", Type = types[i += 1] });
            result.Add(new MiscModel() { Name = "Name 6", Type = types[i += 1] });
            result.Add(new MiscModel() { Name = "Name 7", Type = types[i += 1] });

            return result;
        }

        public SimpleKeyValuePair[] GetAllTypes()
        {
            List<SimpleKeyValuePair> result = new List<SimpleKeyValuePair>();

            result.Add(new SimpleKeyValuePair("Type 1", "Value 1"));
            result.Add(new SimpleKeyValuePair("Type 2", "Value 2"));
            result.Add(new SimpleKeyValuePair("Type 3", "Value 3"));
            result.Add(new SimpleKeyValuePair("Type 4", "Value 4"));
            result.Add(new SimpleKeyValuePair("Type 5", "Value 5"));
            result.Add(new SimpleKeyValuePair("Type 6", "Value 6"));
            result.Add(new SimpleKeyValuePair("Type 7", "Value 7"));

            return result.ToArray();
        }
    }
}


ViewModel

using System.Collections.ObjectModel;
using SLPrismGridComboBox.ModuleA.Models;
namespace SLPrismGridComboBox.ModuleA.ViewModels
{
    public interface IMiscViewModel
    {
        ObservableCollection<MiscModel> Items { get; set; }
        SimpleKeyValuePair[] Types { get; }
    }
}

using System.Collections.ObjectModel;
using System.ComponentModel;
using SLPrismGridComboBox.ModuleA.Models;
using SLPrismGridComboBox.ModuleA.Services;

namespace SLPrismGridComboBox.ModuleA.ViewModels
{
    public class MiscViewModel : IMiscViewModel, INotifyPropertyChanged
    {
        IMiscService _miscService;

        private ObservableCollection<MiscModel> _items;
        public ObservableCollection<MiscModel> Items
        {
            get { return _items; }
            set
            {
                if (_items != value)
                {
                    _items = value;
                    RaisePropertyChanged("Items");
                }
            }
        }

        private SimpleKeyValuePair[] _types;
        public SimpleKeyValuePair[] Types
        {
            get { return _types; }
            private set
            {
                if (_types != value)
                {
                    _types = value;
                    RaisePropertyChanged("Types");
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public MiscViewModel(IMiscService miscService)
        {
            _miscService = miscService;
            this.Items = _miscService.GetAllMiscModels();
            this.Types = _miscService.GetAllTypes();
        }

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

    }
}

View
namespace SLPrismGridComboBox.ModuleA.Views
{
    public interface IMiscView
    {

    }
}

using System.Windows.Controls;
using SLPrismGridComboBox.ModuleA.ViewModels;

namespace SLPrismGridComboBox.ModuleA.Views
{
    public partial class MiscView : UserControl, IMiscView
    {
        IMiscViewModel _viewModel;

        public MiscView(IMiscViewModel viewModel)
        {
            InitializeComponent();

            _viewModel = viewModel;
            this.DataContext = _viewModel;
        }
    }
}

<UserControl x:Class="SLPrismGridComboBox.ModuleA.Views.MiscView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" 
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
        <data:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Path=Items}">
            <data:DataGrid.Columns>
                <data:DataGridTextColumn Header="Name" Binding="{Binding Path=Name}" />
                <data:DataGridTemplateColumn Header="Type">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Path=Type.Key}" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <ComboBox DisplayMemberPath="{Binding Path=Type.Key}" SelectedItem="{Binding Path=Type}" ItemsSource="{Binding Path=Types}" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
            </data:DataGrid.Columns>
        </data:DataGrid>
    </Grid>
</UserControl>
Feb 26, 2010 at 5:33 PM

Hi Ryan,

Cause
As you said this is because your DataGrid has been set with a different DataContext (this action is performed when you set the ItemsSource property for a DataGrid), which doesn’t contain the property you want to bind to. Therefore, the Binding is not able to reach the DataContext within a DataGrid.
That said, as Silverlight doesn’t support RelativeSource binding values other than “Self” and “Templated Parent” (which would be really useful in these cases) you have to perform a workaround.

Possible Workaround
The team Prism Stock Trader RI came up with a possible approach to solve this. You can review it in the following files in the StockTrader.RI.Modules.Position project:

  • PositionSummaryView.xaml: shows how the Click.Command dependency property for the Action buttons is set. They are inside a DataGrid named PositionsGrid. You will notice that the source property has been set with Observable Commands to avoid running into timing issues when the resource is be set and when it is consumed.
  • PositionSummaryView.xaml.cs: the code-behind not only sets its Model property, but also sets the Observable Commands with the same model.

Please let me know if this helps.

Damian Schenkelman
http://blogs.southworks.net/dschenkelman