Search code examples
xamarinxamarin.formscontroltemplatecontroltemplatescontrol-template

Xamarin Forms shared ControlTemplate for ContentPage and CarouselPage


I'm stuck trying to reuse an control template for a independent ContentPage as well as a ContentPage in a CarouselPage...

The main problem is that the CarouselPage doesn't support the ControlTemplate property. Therefore I'm forced to insert a ContentPage in the DataTemplate of the CarouselPage. This ContentPage then can get the ControlTemplate assigned but I run into the problem that the BindingContext is not the root of the ViewModel.

I'll also try to explain the issues with code:

I've create the template as shown below.

<!-- Loader view template -->
<ControlTemplate x:Key="LoaderViewTemplate">
    <AbsoluteLayout Padding="0" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">

        <!-- Content -->
        <ContentPresenter AbsoluteLayout.LayoutBounds="0, 0, 1, 1" AbsoluteLayout.LayoutFlags="All" />

        <!-- Loader -->
        <BoxView IsVisible="{TemplateBinding BindingContext.IsBusy}" BackgroundColor="Green" Opacity="0.5" AbsoluteLayout.LayoutBounds="0, 0, 1, 1" AbsoluteLayout.LayoutFlags="All" />
        <StackLayout IsVisible="{TemplateBinding BindingContext.IsBusy}" Padding="6" BackgroundColor="Gray" Orientation="Horizontal" AbsoluteLayout.LayoutBounds="0.5, 0.5, -1, -1" AbsoluteLayout.LayoutFlags="PositionProportional">
            <ActivityIndicator Color="White" IsRunning="{TemplateBinding BindingContext.IsBusy}" VerticalOptions="Center" WidthRequest="20" HeightRequest="20" />
            <Label TextColor="White" Text="Loading..." VerticalOptions="Center" />
        </StackLayout>

    </AbsoluteLayout>
</ControlTemplate>

The template is working correctly for the ContentPage shown below.

<ContentPage ...
             ControlTemplate="{StaticResource LoaderViewTemplate}">

    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
        ...
    </StackLayout>

</ContentPage>

But it doesn't work in the CarouselPage as shown below.

<CarouselPage ...
              ItemsSource="{Binding Tournament.Rounds}">

    <CarouselPage.ItemTemplate>
        <DataTemplate>
            <ContentPage ControlTemplate="{StaticResource LoaderViewTemplate}">
                ...
            </ContentPage>
        </DataTemplate>
    </CarouselPage.ItemTemplate>

</CarouselPage>

The BindingContext in the CarouselPage becomes a TournamentRoundModel from the Tournament.Rounds collection.

Does any one has an idea on how I can reach the root of the ViewModel within the independent ContentPage and the CarouselPage nested ContentPage?

Kind regards, Jop Middelkamp


Solution

  • First of all if you need each ContentPage in CarousalPage to be able to refer to the root view-model, while providing the same to the ControlTemplate's binding(s).

    Simplest way to do that would be to extend ContentPage to support a bindable property to hold this reference (to root view-model).

    public class ExContentPage : ContentPage
    {
        public static readonly BindableProperty RootViewModelProperty =
            BindableProperty.Create(
                "RootViewModel", typeof(object), typeof(ExContentPage),
                defaultValue: default(object));
    
        public object RootViewModel
        {
            get { return (object)GetValue(RootViewModelProperty); }
            set { SetValue(RootViewModelProperty, value); }
        }
    }
    

    Then you can update your shared control-template as:

    <!-- Loader view template -->
    <ControlTemplate x:Key="LoaderViewTemplate">
        <AbsoluteLayout Padding = "0" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
    
            <!-- Content -->
            <ContentPresenter .. />
    
            < !--Loader-- >
            <BoxView IsVisible= "{TemplateBinding RootViewModel.IsBusy}" BackgroundColor= "Green" .. />
    
            <StackLayout IsVisible= "{TemplateBinding RootViewModel.IsBusy}" .. >
                <ActivityIndicator Color= "White" IsRunning= "{TemplateBinding RootViewModel.IsBusy}"  />
                <Label TextColor= "White" Text= "Loading..." VerticalOptions= "Center" />
            </StackLayout>
        </AbsoluteLayout>
    </ControlTemplate>
    

    Sample usage would look like:

    <local:ExContentPage ...
            xmlns:local="clr-namespace:CustomNamespace"     
            RootViewModel="{Binding}"
             ControlTemplate="{StaticResource LoaderViewTemplate}">
    
        <StackLayout HorizontalOptions = "Center" VerticalOptions="Center">
            ...
        </StackLayout>
    </local:ExContentPage>
    

    and,

    <CarouselPage...
                 x:Name="Parent" 
                 ItemsSource="{Binding Tournament.Rounds}">
        <CarouselPage.ItemTemplate>
            <DataTemplate>
                <local:ExContentPage
                    ControlTemplate = "{StaticResource LoaderViewTemplate}"
                    RootViewModel="{Binding BindingContext, Source={x:Reference Parent}}">
                    ...
                </ContentPage>
            </DataTemplate>
        </CarouselPage.ItemTemplate>
    </CarouselPage>
    

    Furthermore, if IsBusy is the only property that you need to refer in ControlTemplate - you can create an IsBusy bindable property in extended content-page; instead of RootViewModel.