Binding from within a ResourceDictionary in a Cate

2019-07-18 09:55发布

I am converting some of the views and view models of our WPF application over to Catel, as a proof-of-concept.

One of the user controls doesn't seem to be correctly binding to the view model at runtime. I think I understand why that is, but would like to get some feedback on what the best remedy is.

The code

I have a simple view whose model is actually an ObservableCollection:

PersonTable.xaml

Key things to note: I'm using a CollectionViewSource that wraps the main collection that the DataGrid binds to. This is so I can keep the grid auto-sorted.

<catel:UserControl x:Class="MyApp.PersonTable"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
             xmlns:catel="http://catel.codeplex.com"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="200" d:DataContext="{DynamicResource DesignTimeViewModel}">
    <UserControl.Resources>
        <ResourceDictionary>
            <CollectionViewSource Source="{Binding PersonItems}" x:Key="PersonItemsSource">
                <CollectionViewSource.SortDescriptions>
                    <scm:SortDescription PropertyName="DOB" Direction="Descending" />
                </CollectionViewSource.SortDescriptions>
            </CollectionViewSource>

            <ui:DesignPersonViewModel x:Key="DesignTimeViewModel" />
        </ResourceDictionary>
    </UserControl.Resources>

    <Grid>
        <DataGrid ItemsSource="{Binding Source={StaticResource PersonItemsSource}}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name, Mode=TwoWay}" 
                                    Header="Name" Width="90"
                                    ElementStyle="{StaticResource CellRightAlign}" />
                <!-- etc..... -->
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</catel:UserControl>

PersonTableViewModel.cs

The view model accepts the model in the constructor:

using Catel.MVVM;

public class PersonTableViewModel : ViewModelBase
{
    public PersonTableViewModel(ObservableCollection<Person> personItems)
    {
        this.PersonItems = personItems
    }

    public ObservableCollection<Person> PersonItems
    {
        get { return GetValue<ObservableCollection<Person>>(PersonItemsProperty); }
        set { SetValue(PersonItemsProperty, value); }
    }

    public static PropertyData PersonItemsProperty = 
        RegisterProperty("PersonItems", typeof(ObservableCollection<Person>), () => new ObservableCollection<PersonItems>());
}

The Problem

At runtime, no items are populated in the grid. Although at design time, the design view model does correctly populate the grid in the design view.

Am I right about the source of the problem? I believe it's that the control that is bound to the PersonItems property is not part of the visual tree, but is embedded in a control-level resource dictionary? Based on my reading of the documentation, specifically the article UserControl - Under the hood, it seems that the Catel UserControl class injects the view model as a hidden inner DataContext inside the visual tree only, but my {Binding} inside a resource dictionary item might get left out in the cold.

Assuming I'm right, what's the best remedy?

If I'm right about the above, then I can think of a few possible remedies, none of which seem perfect. I would love to know what the accepted best practice is to remedy this situation.

  • Move the CollectionViewSource to the code behind; expose it as a dependency property. I don't love this option because I can't then configure it in XAML.
  • Move the CollectionViewSource to the view model. I really don't love this one; putting WPF components in the view model breaks MVVM.
  • Bind the CollectionViewSource to the original DataContext (i.e. the model). The problem there is that then the design-time view model would not bind correctly.

    <CollectionViewSource Source="{Binding}" ..... >
    
  • Expose a dependency property from the code-behind that is bound to the view model. UPDATE: this works at runtime, but now fails at design time (in that the grid does not contain the test data.)

    ---- PersonTable.xaml.cs ----
    
    [ViewToViewModel(MappingType = ViewToViewModelMappingType.ViewModelToView]
    public ObservableCollection<PersonItem> PersonItems { get { ... } }
    
    ---- PersonTable.xaml ----
    
    <CollectionViewSource Source="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type PersonTable}}, Path=PersonItems}" ...... >
    

标签: wpf mvvm catel
1条回答
▲ chillily
2楼-- · 2019-07-18 10:29

Your assumptions are all correct. But there is a 4th remedy. Put the Resources inside the Grid so you are inside the ViewModel data context:

<catel:UserControl x:Class="MyApp.PersonTable"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
             xmlns:catel="http://catel.codeplex.com"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="200" d:DataContext="{DynamicResource DesignTimeViewModel}">

    <Grid>
        <Grid.Resources>
            <ResourceDictionary>
                <CollectionViewSource Source="{Binding PersonItems}" x:Key="PersonItemsSource">
                    <CollectionViewSource.SortDescriptions>
                        <scm:SortDescription PropertyName="DOB" Direction="Descending" />
                    </CollectionViewSource.SortDescriptions>
                </CollectionViewSource>

                <ui:DesignPersonViewModel x:Key="DesignTimeViewModel" />
            </ResourceDictionary>
        </Grid.Resources>

        <DataGrid ItemsSource="{Binding Source={StaticResource PersonItemsSource}}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name, Mode=TwoWay}" 
                                    Header="Name" Width="90"
                                    ElementStyle="{StaticResource CellRightAlign}" />
                <!-- etc..... -->
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</catel:UserControl>
查看更多
登录 后发表回答