Search code examples
c#xamlmvvmwinui-3windows-app-sdk

Using the same view for different ViewModels in WinUI3


In a WinUI3 project, I have 3 views IconEditorView, ColorEditorView, and ImageEditorView, each with their corresponding ViewModel IconEditorViewModel, ColorEditorViewModel, and ImageEditorViewModel. The views are practically the same, the only differences are a few labels that are binded from the ViewModel anyways (the specific UI elements are in independent UserControls within the views).

I was wondering if there is any way to be able to have a generic view like EditorView and attach the corresponding ViewModel. Since they all implement IEditorViewModel they all have everything that would be necessary for EditorView to work. Currently, for each view, I attach the corresponding ViewModel by using a dependency property like so:

public sealed partial class ImageEditorView : UserControl
{
    public ImageEditorViewModel ImageEditorViewModel
    {
        get => (ImageEditorViewModel)GetValue(ImageEditorViewModelProperty);
        set => SetValue(ImageEditorViewModelProperty, value);
    }

    public static readonly DependencyProperty ImageEditorViewModelProperty =
    DependencyProperty.Register(
        nameof(ImageEditorViewModel),
        typeof(ImageEditorViewModel),
        typeof(ImageEditorView),
        new PropertyMetadata(default));

    public ImageEditorView(ImageEditorViewModel imageEditorViewModel)
    {
        this.InitializeComponent();
        ImageEditorViewModel = imageEditorViewModel;
    } 
}

Silly me tried converting the ViewModel property type to an IEditorViewModel, but that, of course, did not work since I needed data binding for updating a few workflow indicators as the user interacts with the view.

In the future, I will likely add new editors, so this approach would be very convenient to just create the ViewModel implementing IEditorViewModel and attach it to an instance of the existing generic EditorView.

How can I achieve this?

Note: I'm using WinUI3 latest stable version along with CommunityToolkit.MVVM if that's relevant in any way.

Thanks!


Solution

  • You could use DataTemplateSelector for this.

    Let's say we have these editor classes:

    public interface IEditor
    {
        string Name { get; }
    }
    
    public class IconEditor : IEditor
    {
        public string Name { get; } = nameof(IconEditor);
    
        public Symbol Icon { get; set; }
    }
    
    public class ColorEditor : IEditor
    {
        public string Name { get; } = nameof(ColorEditor);
    
        public Brush Color { get; set; } = new SolidColorBrush(Colors.Transparent);
    }
    

    then the custom control could be:

    EditorView.cs

    public class EditorView : Control
    {
        public static readonly DependencyProperty EditorProperty =
            DependencyProperty.Register(nameof(Editor),
                typeof(IEditor),
                typeof(EditorView),
                new PropertyMetadata(default));
    
        public static readonly DependencyProperty EditorTemplateSelectorProperty =
            DependencyProperty.Register(
                nameof(EditorTemplateSelector),
                typeof(DataTemplateSelector),
                typeof(EditorView),
                new PropertyMetadata(default));
    
        public EditorView()
        {
            DefaultStyleKey = typeof(EditorView);
        }
    
        public ContentPresenter? EditorPresenter { get; set; }
    
        public IEditor Editor
        {
            get => (IEditor)GetValue(EditorProperty);
            set => SetValue(EditorProperty, value);
        }
        public DataTemplateSelector EditorTemplateSelector
        {
            get => (DataTemplateSelector)GetValue(EditorTemplateSelectorProperty);
            set => SetValue(EditorTemplateSelectorProperty, value);
        }
    }
    

    Generic.xaml

    <ResourceDictionary
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:App1">
    
        <Style TargetType="local:EditorView">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:EditorView">
                        <Border
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                            <ContentControl
                                Content="{TemplateBinding Editor}"
                                ContentTemplateSelector="{TemplateBinding EditorTemplateSelector}" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    
    </ResourceDictionary>
    

    and the DataTemplateSelector could be something like this:

    DataTemplateSelector with Dictionary in XAML

    public class StringToDataTemplateDictionary : Dictionary<string, DataTemplate>
    {
    }
    
    public class EditorTemplateSelector : DataTemplateSelector
    {
        public DataTemplate DefaultTemplate { get; set; } = new();
    
        public StringToDataTemplateDictionary DataTemplates { get; set; } = [];
    
        protected override DataTemplate SelectTemplateCore(object item)
        {
            return base.SelectTemplateCore(item);
        }
    
        protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
        {
            return item is IEditor editor
                ? DataTemplates.TryGetValue(editor.Name, out var template) is true
                    ? template
                    : DefaultTemplate
                : DefaultTemplate;
        }
    }
    

    finally, we can use the control:

    <Page.Resources>
        <local:EditorTemplateSelector x:Key="EditorTemplateSelector">
            <local:EditorTemplateSelector.DataTemplates>
                <DataTemplate
                    x:Key="IconEditor"
                    x:DataType="local:IconEditor">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{x:Bind Name}" />
                        <SymbolIcon Symbol="{x:Bind Icon}" />
                    </StackPanel>
                </DataTemplate>
                <DataTemplate
                    x:Key="ColorEditor"
                    x:DataType="local:ColorEditor">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{x:Bind Name}" />
                        <Rectangle
                            Width="32"
                            Height="32"
                            Fill="{x:Bind Color}" />
                    </StackPanel>
                </DataTemplate>
            </local:EditorTemplateSelector.DataTemplates>
        </local:EditorTemplateSelector>
    </Page.Resources>
    
    <StackPanel>
        <Button
            Click="ToggleEditorButton_Click"
            Content="Toggle Editor" />
        <local:EditorView
            x:Name="EditorViewControl"
            EditorTemplateSelector="{StaticResource EditorTemplateSelector}" />
    </StackPanel>
    
    private void ToggleEditorButton_Click(object sender, RoutedEventArgs e)
    {
        EditorViewControl.Editor = EditorViewControl.Editor is IconEditor
            ? new ColorEditor() { Color = new SolidColorBrush(Colors.SkyBlue) }
            : new IconEditor() { Icon = Symbol.Home };
    }