Can I bind to a view model from within a DataTemplate using x:Bind?

180 Views Asked by At

Bare minimum example:

<DataTemplate x:Key="CustomTemplate" x:DataType="data:CustomType">
    <TextBlock Text="{x:Bind Foo}" Foreground="{x:Bind viewModel.GetColor(Foo), Mode=OneWay}" />
    <TextBlock Text="{x:Bind Bar}" />
</DataTemplate>

In this code, the viewModel cannot be found because the data template is looking for a viewModel within CustomType. This uses a feature of x:Bind which allows functions to be used in bindings. This means that I can't use RelativeSource with a Binding to set the path where the binding looks for viewModel (though I couldn't get it to work even without the function).

There are workarounds that would be pretty easy. For example, since CustomType is an ObservableObject it's no problem to just create a Color property which the binding can read directly, but because CustomType is just a data type it shouldn't care about what's going on in the UI side of things at all. Alternatively, a wrapper class (CustomTypeUi or such) could expose that additional functionality, but a wrapper just for something like this which seems like it should be pretty trivial feels wrong and I'd like to avoid it if possible.

Is there an idiomatic approach to solving this problem?

4

There are 4 best solutions below

1
Andrew KeepCoding On

If the DataTemplate is a resource of a Page, you can x:Name your Page, ThisPage for instance, and then:

<DataTemplate>
    <TextBlock Text="{Binding ElementName=ThisPage, Path=ViewModel.AwesomeString, Mode=OneWay}" />
</DataTemplate>

But:

  • works with Binding but not with x:Bind, so cannot call a method.
  • works with ListView or GridView but not with ItemsRepeater.

Now, let me show you an example using a value converter. This example relies on that Foo is a string or that ToString() can be used but you might be able to apply it to your case.

public class  StringToColorDictionary : Dictionary<string, Color>
{
}
public class ObjectToColorConverter : IValueConverter
{
    public StringToColorDictionary Colors { get; set; } = new();

    public object Convert(object value, Type targetType, object parameter, string language)
    {
        return
            value.ToString() is string stringValue &&
            Colors.TryGetValue(stringValue, out var color)
                ? new SolidColorBrush(color)
                : new SolidColorBrush(Microsoft.UI.Colors.Transparent);
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}
<Page.Resources>
    <local:ObjectToColorConverter x:Key="ObjectToColorConverter">
        <local:ObjectToColorConverter.Colors>
            <local:StringToColorDictionary>
                <Color x:Key="A">Red</Color>
                <Color x:Key="B">Green</Color>
                <Color x:Key="C">Blue</Color>
            </local:StringToColorDictionary>
        </local:ObjectToColorConverter.Colors>
    </local:ObjectToColorConverter>
    <DataTemplate
        x:Key="ItemTemplate"
        x:DataType="local:Item">
        <TextBlock
            Foreground="{x:Bind Foo, Converter={StaticResource ObjectToColorConverter}}"
            Text="{x:Bind Foo}" />
    </DataTemplate>
</Page.Resources>

<ListView
    ItemTemplate="{StaticResource ItemTemplate}"
    ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}" />
1
YangXiaoPo-MSFT On

Another way is to bind this FrameworkElement.DataContext to view Element. See @mm8 's answer https://stackoverflow.com/a/70703773. But It seems the view cannot be Microsoft.UI.Xaml.Window.

My test:

<ResourceDictionary>
    <DataTemplate x:Key="MyDataTemplate" x:DataType="local:Customer">
        <StackPanel Orientation="Horizontal">
            <TextBlock local:AncestorSource.AncestorType="ListBox" Text="{Binding Tag}" />
            
            ...
        </StackPanel>
    </DataTemplate>
</ResourceDictionary>

and

<Window
    x:Class="App2.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App2"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <ListBox ItemsSource="{x:Bind Customers}" Width="350" Margin="0,5,0,10" ItemTemplate="{StaticResource MyDataTemplate}" Tag="aTag"/>
    </StackPanel>
</Window>
1
YangXiaoPo-MSFT On

This unfortunately requires use of Binding over x:Bind, which makes using a function not possible.

@Poyo OK, a new question. Have you checked @mm8 's BindingToParentElementSample sample?

And I also have my own way to do this. You can set x:Binds' dataRoot in your name.g.cs file but you must determine which is your dataRoot for your x:Binds in the same DataTemplate. See https://learn.microsoft.com/en-us/answers/questions/1465387/generic-xaml-x-bind-with-converter-binding for more information

public bool SetDataRoot(global::System.Object newDataRoot)
{
    this.bindingsTracking.ReleaseAllListeners();
    if (newDataRoot != null)
    {
        this.dataRoot = global::WinRT.CastExtensions.As<global::App3.DateRange>(newDataRoot);
        return true;
    }
    return false;
}

enter image description here

0
Gerry Schmitz On

I've simply resorted to "functional programming" in my "data objects" (x:DataType templates or anywhere else) when confronted with these brain teasers; as in:

public string MyAnswer => this.IsFoo ? this.Name : SomethingElse();  // etc.

Then "x:Bind" to MyAnswer and / or use it in procedural programming.

Using variations such as "IsThisOrThat => ...", etc. also improves readability.