Kux
Kux

Reputation: 1479

How to present data with binding and DataTemplate -or- ContentControl for MAUI

How to present a string, number or a also view model with binding and DataTemplate?

I am looking for a MAUI replacement for the WPF ContentControl.

The ContentView has a Content property but this is from type View. The ContentPresenter has a Content property but this is also from type View. <Ignorable>WTF, Why this is not named ViewPresenter when it can only present a View??? Someoteimes MAUI is weird.</Ignorable>

How to present any content with defining DataTemplates for each data type?

class PropertyViewModel {
   public string Name {get;set;}
   public object Value {get;set;}
}
<Page.Resources>
    <DataTemplate DataType="System.String">
        <Entry Text="{Binding}/>
    </DataTemplate>
    <DataTemplate DataType="System.Int32">
        <NumberPicker Value="{Binding}/>
    </DataTemplate>
    .. more templates, eg. DatePicker for System.DateOnly
</Page.Resources>

<DockLayout>
    <Label Text="{Binding Name}
    <TemplatedContenView Content={Binding Value}/> 
</DockPanel>

The TemplatedContenView or ContentControl (that does not exist in MAUI), can use different templates for different types of Value. In WPF the ContentControl uses ContentTemplate, ContentTemplateSelector or if none specified it looked into the resources to find the template.

<Ignorable>I often have the feeling with MAUI that I have to constantly reinvent things that are standard in WPF. Yes I know MAUI is not WPF, but there should still be at least similar concepts. The switch from WinForms to WPF was much easier and the differences were considerably greater.</Ignorable>

Edit1: a more detailed example

Upvotes: 8

Views: 6497

Answers (2)

Marc_Alx
Marc_Alx

Reputation: 1441

Circumventing ItemTemplateSelector as suggested by Alex is clever, but introduces some delays perceptible to end user (a few ms). This is particularly visible when done in CollectionView.

One way to acheive conditional template binding is to use ControlTemplate.

This way you can:

  1. Create a ControlTemplate for each of your use case in a ResourceDictionary.
  2. In your view model add a ControlTemplate property that fetch the correct ControlTemplate from your resource according to your business logic.
  3. In your xaml consume your template asis <ContentView ControlTemplate="{Binding ViewModel.Template}"/>

Upvotes: 0

Artem Tymchenko
Artem Tymchenko

Reputation: 111

I'm a WPF developer and recently I've started MAUI project. And It looks like you have to reinvent the wheel every time when you are going to write such a simple scenario as you mentioned :(. When you do it using WPF you even don't need to thought about that, it's too easy to implement, but when you use MAUI you should break your mind to do such minor things.

I also encountered the same issue and I didn't find a simple in-box solution. But I came up with the idea to create a control with some layout inside that has attached properties from BindableLayout

TemplatedContentPresenter.xaml.cs:

public partial class TemplatedContentPresenter : ContentView
{
    public TemplatedContentPresenter()
    {
        InitializeComponent();
    }

    public static readonly BindableProperty DataTemplateSelectorProperty = BindableProperty.Create(nameof(DataTemplateSelector), typeof(DataTemplateSelector), typeof(TemplatedContentPresenter), null, propertyChanged: DataTemplateSelectorChanged);
    public static readonly BindableProperty DataTemplateProperty = BindableProperty.Create(nameof(DataTemplate), typeof(DataTemplate), typeof(TemplatedContentPresenter), null, propertyChanged: DataTemplateChanged);
    public static readonly BindableProperty DataProperty = BindableProperty.Create(nameof(Data), typeof(object), typeof(TemplatedContentPresenter), null, propertyChanged: DataChanged);

    public DataTemplateSelector DataTemplateSelector
    {
        get =>(DataTemplateSelector)GetValue(DataTemplateSelectorProperty);
        set => SetValue(DataTemplateSelectorProperty, value);
    }

    public DataTemplate DataTemplate
    {
        get => (DataTemplate)GetValue(DataTemplateProperty);
        set => SetValue(DataTemplateProperty, value);
    }

    public object Data
    {
        get => GetValue(DataProperty);
        set => SetValue(DataProperty, value);
    }

    private static void DataTemplateSelectorChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if(bindable is TemplatedContentPresenter contentPresenter && newValue is DataTemplateSelector dataTemplateSelector)
        {
            BindableLayout.SetItemTemplateSelector(contentPresenter.HostGrid, dataTemplateSelector);
        }
    }
    private static void DataTemplateChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is TemplatedContentPresenter contentPresenter && newValue is DataTemplate dataTemplate)
        {
            BindableLayout.SetItemTemplate(contentPresenter.HostGrid, dataTemplate);
        }
    }

    private static void DataChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is TemplatedContentPresenter contentPresenter)
        {
            BindableLayout.SetItemsSource(contentPresenter.HostGrid, new object[] { newValue });
        }
    }
}

TemplatedContentPresenter.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyApp.TemplatedContentPresenter">
    <Grid x:Name="HostGrid" x:FieldModifier="private" />
</ContentView>

Usage:

<Frame WidthRequest="500" HeightRequest="500">
     <controls:TemplatedContentPresenter 
               Data="{Binding}" 
               DataTemplateSelector="{StaticResource CardTemplateSelector}"/>
</Frame>

UPD: While I was writing the answer I came up with another solution with a simple converter: SingleObjectToArray.xaml

    internal class SingleObjectToArray : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return new object[] { value };
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

Usage:

<Frame>
    <Frame.Resources>
        <converters:SingleObjectToArray x:Key="SingleObjectToArrayConverter"/>
    </Frame.Resources>
    <Grid BindableLayout.ItemsSource="{Binding Converter={StaticResource SingleObjectToArrayConverter}}"
          BindableLayout.ItemTemplateSelector="{StaticResource CardTemplateSelector}" /> 
</Frame>

Upvotes: 10

Related Questions