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!
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 };
}