Home > OS >  Add custom content inside a ContentView using XAML
Add custom content inside a ContentView using XAML

Time:01-07

I want to create reusable components with a nice XAML markup. To train myself I am doing a collapse component similar to the Expander of .NET MAUI Community Toolkit, the goal is to have the same kind of usage :

<Expander>
    <Expander.Header>
        <Label Text="Baboon"
               FontAttributes="Bold"
               FontSize="Medium" />
    </Expander.Header>
    <HorizontalStackLayout Padding="10">
        ...
    </HorizontalStackLayout>
</Expander>

In the github of the Expander they do everything in C# so that doesn't help me much. Looking at WPF and Xamarin doc I found that you need to use ContentPresenter to pass some content, but I can't make something working with them.

C# class :

[ContentProperty(nameof(CollapseContent))]
public partial class Collapse : ContentView
{
    public static readonly BindableProperty HeaderProperty
    = BindableProperty.Create(nameof(Header), typeof(IView), typeof(Collapse));

    public IView Header
    {
        get => (IView)GetValue(HeaderProperty);
        set => SetValue(HeaderProperty, value);
    }

    public static readonly BindableProperty CollapseContentProp
    = BindableProperty.Create(nameof(CollapseContent), typeof(IView), typeof(Collapse));

    public IView CollapseContent
    {
        get => (IView)GetValue(CollapseContentProp);
        set => SetValue(CollapseContentProp, value);
    }

    public Collapse()
    {
        InitializeComponent();
    }
}

The XAML :

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Cubetrail.Maui.Shared.Collapse">
    <ContentView.Content>
        <Grid ColumnDefinitions="*,100" RowDefinitions="40,auto">
            <ContentPresenter Content="{TemplateBinding Header}"/>
            <Button x:Name="Btn_Toggle"
            Grid.Column="1" 
            Text="Test"
            Clicked="Expand_Clicked"></Button>

            <ContentPresenter Content="{TemplateBinding CollapseContent}" x:Name="ContenuContaineur"
              Grid.Row="1" Grid.ColumnSpan="2"/>
        </Grid>
    </ContentView.Content>
</ContentView>

And I use it like this :

<composants:Collapse>
    <composants:Collapse.Header>
        <Label>Header</Label>
    </composants:Collapse.Header>
    <Label>Body</Label>
</composants:Collapse>

The setter of the properties never trigger and the Content is not shown, from what I have seen it should work like this. I ended up doing something else that does work but feel like a hack :

C#, I added the propertyChanged event :

public static readonly BindableProperty HeaderProperty
= BindableProperty.Create(nameof(Header), typeof(IView), typeof(Collapse), propertyChanged: OnHeaderPropertyChanged);

static void OnHeaderPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
    var lCollapse = (Collapse)bindable;
    if (newValue is View view)
        lCollapse.HeaderContaineur.Content = view;
}

And in the XAML I remplaced the ContentPresenter with this :

<ContentView x:Name="HeaderContaineur"/>

TLDR : How do you properly pass content to a custom component (find an equivalent to blazor RenderFragment) ?

CodePudding user response:

I am really sorry, but nothing in your code is even close to what you need to do.

If you see Expander. It has a grid, with two rows. (One is header, the other content). The logic behind this is modifying this grid to manipulate the state of that Expander.

You really need to start from the constructor. When you add the grid and tap gesture, you can start writing the bindings.

A binding like this:

 public static readonly BindableProperty HeaderProperty
= BindableProperty.Create(nameof(Header), typeof(IView), typeof(Collapse));

Is almost of no use. The key to do this binding is to call some code, when a change is detected. This is why you see all those OnPropertyChanged methods.

(You will see how this grid is manipulated for removing the old view and adding the new view.)

It is not that complicated, but you have to understand what is going on behind this code, and principles of binding and commanding to reproduce it successfully.

And at the end collapser is what exactly? Expander that starts with IsExpanded to true? I wouldn't duplicate code in my project. Tomorrow when I need to make a change, I have to change it at both places. Please, no.

CodePudding user response:

There is no easy XAML syntax to bind content, you have to do it in C# and manage everything yourself with BindableProperty and propertyChanged

public static readonly BindableProperty HeaderProperty
= BindableProperty.Create(nameof(Header), typeof(IView), typeof(Collapse), propertyChanged: OnHeaderPropertyChanged);

As @ToolmakerSteve said you can sort of use Control templates, wich means having a syntax similar to that :

<ContentPage.Resources>
    <ControlTemplate x:Key="Collapse">
        <Grid RowDefinitions="*,*">
            <Label>Test :</Label>
            <ContentPresenter BackgroundColor="Red" Grid.Row="1" />
        </Grid>
    </ControlTemplate>
</ContentPage.Resources>
...
<components:Collapse ControlTemplate="{StaticResource Collapse}">
    <Label>Content</Label>
</components:Collapse>

Basically you have your view inside a ResourceDictionary and you overwrite the existing view ...

It's so frustrating, in comparaison you can have the same result in .net maui blazor with just :

<p>Test :</p>
<div>
    @ChildContent
</div>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }
}

<!-- Usage -->
<Component>Body<Component/>
  • Related