WinUI 3 How to Bind Command to ViewModel property when using a DataTemplate?

Viewed 1346

I am using a Grid called MainGrid to position an ItemsRepeater whose ItemsSource is bound to an ObservableCollection within my ViewModel.

<muxc:ItemsRepeater
    ItemsSource="{Binding Path=Molts}"
    Layout="{StaticResource VerticalStackLayout}"
    ItemTemplate="{StaticResource MoltTemplate}">
</muxc:ItemsRepeater>

I have created a DataTemplate

<DataTemplate x:Key="MoltTemplate">
    <StackPanel Orientation="Horizontal">
        <TextBlock x:Name="text" Text="{Binding ID}"></TextBlock>
        <Button Command="{Binding DisplayAvailAIsCommand}" CommandParameter="{Binding ElementName=text, Path=Text}">Add</Button>
    </StackPanel>
</DataTemplate>

which has a TextBox and Button. I want the Button to fire a command in my ViewModel but items within the ItemsRepeater have their DataContext set to their Model class and not the ViewModel. I found this post which states that I can change the Command of my Button to set the DataContext to my ViewModel by setting ElementName to a UI element that has as its DataContext the ViewModel

<Button Command="{Binding DataContext.DisplayAvailAIsCommand, ElementName=MainGrid}" CommandParameter="{Binding ElementName=text, Path=Text}">Add</Button>

The constructor of my window sets the DataContext of the MainGrid like this

public MainWindow()
{
    this.InitializeComponent();
    MainGrid.DataContext = new MoltViewModel();
}

However, this does not work and the command does not fire. What am I doing wrong?

1 Answers

You could create an attached property that sets the DataContext of the Button to a parent element of a specific type such as for example ItemsRepeater:

public static class AncestorSource
{
    public static readonly DependencyProperty AncestorTypeProperty =
        DependencyProperty.RegisterAttached(
            "AncestorType",
            typeof(Type),
            typeof(AncestorSource),
            new PropertyMetadata(default(Type), OnAncestorTypeChanged)
    );

    public static void SetAncestorType(FrameworkElement element, Type value) =>
        element.SetValue(AncestorTypeProperty, value);

    public static Type GetAncestorType(FrameworkElement element) =>
        (Type)element.GetValue(AncestorTypeProperty);

    private static void OnAncestorTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement target = (FrameworkElement)d;
        if (target.IsLoaded)
            SetDataContext(target);
        else
            target.Loaded += OnTargetLoaded;
    }

    private static void OnTargetLoaded(object sender, RoutedEventArgs e)
    {
        FrameworkElement target = (FrameworkElement)sender;
        target.Loaded -= OnTargetLoaded;
        SetDataContext(target);
    }

    private static void SetDataContext(FrameworkElement target)
    {
        Type ancestorType = GetAncestorType(target);
        if (ancestorType != null)
            target.DataContext = FindParent(target, ancestorType);
    }

    private static object FindParent(DependencyObject dependencyObject, Type ancestorType)
    {
        DependencyObject parent = VisualTreeHelper.GetParent(dependencyObject);
        if (parent == null)
            return null;

        if (ancestorType.IsAssignableFrom(parent.GetType()))
            return parent;

        return FindParent(parent, ancestorType);
    }
}

Usage:

<DataTemplate x:Key="MoltTemplate">
    <StackPanel Orientation="Horizontal">
        <TextBlock x:Name="text" Text="{Binding ID}"></TextBlock>
        <Button local:AncestorSource.AncestorType="muxc:ItemsRepeater"
                Command="{Binding DataContext.DisplayAvailAIsCommand}"
                CommandParameter="{Binding ElementName=text, Path=Text}">Add</Button>
    </StackPanel>
</DataTemplate>

Please refer to this blog post for more information.

Related