Binding generic type through MVVM Light RelayCommand

1.4k views Asked by At

I am using Implicit Data Templates which are applied to the items of an ItemsControl:

<ItemsControl ItemsSource="{Binding Path=CategoryAttributeVMs}"/>

<DataTemplate DataType="{x:Type my:CategoryAttributeDateFieldVM}">
    <DockPanel>
        <.....>
        <Button Command="{Binding Path=MainViewModel.CopyToChildrenCommand,
                                  Source={StaticResource Locator}}"
                CommandParameter="{Binding Mode=OneWay}">
    </DockPanel>
</DataTemplate>

<DataTemplate DataType="{x:Type my:CategoryAttributeIntegerFieldVM}">
    <DockPanel>
        <.....>
        <Button Command="{Binding Path=MainViewModel.CopyToChildrenCommand,
                                  Source={StaticResource Locator}}"
                CommandParameter="{Binding Mode=OneWay}">
    </DockPanel>
</DataTemplate>

This is databound to my ViewModel like so:

public abstract class CategoryAttributeVM
{
    protected CategoryAttributeVM(ICategoryAttribute categoryAttribute)
    {
        _categoryAttribute = categoryAttribute;
    }

    private readonly ICategoryAttribute _categoryAttribute;

    public string Name { get { return _categoryAttribute.Name; } }
    public object Value { get { return _categoryAttribute.Value; } }
}

public abstract class CategoryAttributeVM<T> : CategoryAttributeVM
{
    protected CategoryAttributeVM(ICategoryAttribute<T> categoryAttribute)
        : base(categoryAttribute) { _categoryAttribute = categoryAttribute; }

    private readonly ICategoryAttribute<T> _categoryAttribute;

    public new T Value
    {
        get { return _categoryAttribute.Value; }
        set { _categoryAttribute.Value = value; }
    }
}

public class CategoryAttributeDateFieldVM : CategoryAttributeVM<DateTime?>
{
    public CategoryAttributeDateFieldVM(ICategoryAttributeDateField categoryAttributeDateField)
        : base(categoryAttributeDateField) { }
}

public class CategoryAttributeIntegerFieldVM: CategoryAttributeVM<Int32?>
{
    public CategoryAttributeIntegerFieldVM(ICategoryAttributeIntegerField categoryAttributeIntegerField)
       : base(categoryAttributeIntegerField) { }
}

public class MainViewModel : ViewModelBase
{
    public MainViewModel()
    {
        CategoryAttributeVMs = new List<CategoryAttributeVM>( new CategoryAttributeVM[]
            {
                new CategoryAttributeDateFieldVM(new CategoryAttributeDateField("DateField", DateTime.Now)),
                new CategoryAttributeIntegerFieldVM(new CategoryAttributeIntegerField("IntField", 123))
            });

        CategoryAttributeVM2s = new List<CategoryAttributeVM>(new CategoryAttributeVM[]
            {
                new CategoryAttributeDateFieldVM(new CategoryAttributeDateField("DateField", null)),
                new CategoryAttributeIntegerFieldVM(new CategoryAttributeIntegerField("IntField", null))
            });

        CopyToChildrenCommand = new RelayCommand<CategoryAttributeVM>(DoCopyToChildrenCommand);
    }

    public IEnumerable<CategoryAttributeVM> CategoryAttributeVMs { get; private set; }
    private IEnumerable<CategoryAttributeVM> CategoryAttributeVM2s { get; private set; }

    // This an MVVM Light RelayCommand
    public RelayCommand<CategoryAttributeVM> CopyToChildrenCommand { get; private set; }

    private void DoCopyToChildrenCommand(CategoryAttributeVM ca)
    {
        foreach (var item in CategoryAttributeVM2s)
        {
            // How to copy the ca.Value to item.Value where ca and item are of the same type?
            if (item.GetType() == ca.GetType())
                item.Value = ca.Value;  // !! No worky because item.Value refers to the CategoryAttributeVM.Value, which is a readonly Object property
        }
    }
}

How can I get the objects from CategoryAttributeVM2s that match the type of 'ca'? I think I am missing something elementary, but cannot see it.

The generics approach would be nice but AFAIK the RelayCommand cannot be made generic in that way:

// This would be great
private void DoCopyToChildrenCommand<T, U>(T ca) where T : CategoryAttributeVM<U>
{
    foreach (var item in CategoryAttributeVM2s.OfType<T>())
        item.Value = ca.Value;  // item.Value is type U, which is perfect
}

// But this can't be, obviously
public RelayCommand<CategoryAttributeVM<U>> CopyToChildrenCommand<U> { get; private set; }

Any ideas?

2

There are 2 answers

0
Seb Boulet On BEST ANSWER

In the end, I added an overload of an abstract method on CategoryAttributeVM and CategoryAttributeVM<T>:

public abstract class CategoryAttributeVM 
{ 
    <...>
    public abstract void UpdateValueFrom(CategoryAttributeVM sourceCategoryAttributeVM);
} 

public abstract class CategoryAttributeVM<T> : CategoryAttributeVM 
{
    <...>
    public override void UpdateValueFrom(CategoryAttributeVM sourceCategoryAttributeVM)
    {
        if (GetType() == sourceCategoryAttributeVM.GetType())
            Value = (T)sourceCategoryAttributeVM.Value;
    }
}

I could then use this in DoCopyToChildrenCommand:

private void DoCopyToChildrenCommand(CategoryAttributeVM ca)
{
    foreach (var attribute in CategoryAttributeVM2s)
        attribute.UpdateValueFrom(ca);
}
1
Rachel On

I'm not 100% sure I understand your question, but why not use object in your RelayCommand and make the CanExecute() method return true only if the object can be cast to the your generic type?

ICommand MyCommand { get; set; }

MyCommand = new RelayCommand<object>(
    param => DoCopyToChildrenCommand<T, U>(param as T),
    param => param is T);