AndrewJE
AndrewJE

Reputation: 868

Binding to parent Context using RelativeSource in a DataTemplate (MAUI, XAML)

I have a MAUI .Net 8 project. I have a DataTemplate which is used in the SyncFusion ListView control. The dataTemplate is defined in a Style XAML file in my Resources folder of the project since the DataTemplate is used by more than one page. I am trying to Bind a Command on the DataTemplate to the ViewModel that holds the list of items on the page. I can't seem to get the RelativeSource binding to work. I am using the MAUI ComminityToolkit to us the EventToCommand behaviour.

DataTemplate (Defined in the Resources\Styles folder of the project - File is called MyStyles.xaml)

 <DataTemplate x:Key="JobListItem">
    <ViewCell>
        <effectsView:SfEffectsView x:Name="JobListTouch" TouchDownEffects="Ripple" 
               FadeOutRipple="True" RippleAnimationDuration="300">
           <effectsView:SfEffectsView.Behaviors>
              <toolkit:EventToCommandBehavior EventName="AnimationCompleted"
                   Command="{Binding Source={RelativeSource AncestorType={x:Type 
                      listView:SfListView}}, Path=ShowJobCommand}"/>
        </effectsView:SfEffectsView.Behaviors>
    </ViewCell>
 </DataTemplate

Page hosting the ListView (JobsPage.xaml)

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:listView="clr-namespace:Syncfusion.Maui.ListView;assembly=Syncfusion.Maui.ListView"/>
<Grid>
    <listView:SfListView
                    x:Name="JobListView" AutoFitMode="Height" HorizontalOptions="CenterAndExpand" ItemTemplate="{StaticResource JobListItem}" SelectionMode="None" ItemTapped="JobListView_ItemTapped"
                                           ItemsSource="{Binding JobsSummary}">

</listView:SfListView>

.....
</Grid>
</ContentPage>

The JobsPage is bound to a ViewModel called JobsViewModel. The Command I am trying to bind to in the DataTemplate (ShowJobCommand) is defined on this JobsViewModel as is the List of Jobs (JobsSummary).

The question is how to bind the EventToCommand behaviour to the JobsViewModel?

Upvotes: 0

Views: 69

Answers (1)

IV.
IV.

Reputation: 9438

I think I understand what you're asking: having a data template in "some common space" means that it won't be able to traverse up the visual tree to find its template parent. What may be the proverbial "path of least resistance" here is to let the data template use its own binding, and sort it out in the data model. Do you think something like this might work in your case? For example:

<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:bind_command_from_app_resource"
             x:Class="bind_command_from_app_resource.App">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
                <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
            </ResourceDictionary.MergedDictionaries>
            <DataTemplate x:Key="Common">
                <Frame Padding="10" Margin="5" BorderColor="Gray">
                    <VerticalStackLayout>
                        <Label Text="{Binding JobId}" FontSize="20" FontAttributes="Bold" />
                        <Label Text="{Binding Description}" FontSize="18" />
                        <Label Text="{Binding Client}" FontSize="16" TextColor="Gray" />
                        <Button
                            BackgroundColor="CornflowerBlue"
                            TextColor="WhiteSmoke"
                            WidthRequest="100"
                            HeightRequest="20"
                            HorizontalOptions="End"
                            Text="Edit Job" 
                            Command="{Binding EditJobCommand}"
                            CommandParameter="{Binding .}"/>
                    </VerticalStackLayout>
                </Frame>
            </DataTemplate>

        </ResourceDictionary>
    </Application.Resources>
</Application>

Now obtain JobsViewModel here:

public class Job : INotifyPropertyChanged
{
    public Job()
    {
        EditJobCommand = new Command<Job>(OnEditJob);
    }
    public ICommand EditJobCommand { get; }

    /// <summary>
    /// Figure out e.g. where to navigate here, in the data model
    /// </summary>
    private void OnEditJob(Job job)
    {
        if( Shell.Current?.CurrentPage is MainPage page &&
            page.BindingContext is JobsViewModel jobsVM)
        {
            // HERE YOU GO! Choices:
            // - Redirect to a command in JobsViewModel
            // - Detect that "some different page" is current and direct it there.
            // - Navigate directly to a view that manages the action
            // - And so on...
        }
    }

    public int JobId { get; set; }
    public string? Description { get; set; }
    public string? Client { get; set; }

    public event PropertyChangedEventHandler? PropertyChanged;
}

For Example

The binding problem could be solved in the same general way even though you're using the SyncFusion.ListView and this shows Microsoft.Maui.Controls.CollectionView.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:bind_command_from_app_resource"
             x:Class="bind_command_from_app_resource.MainPage">

    <ContentPage.BindingContext>
        <local:JobsViewModel />
    </ContentPage.BindingContext>
    <ScrollView>
        <VerticalStackLayout
            Padding="30,0"
            Spacing="25">
            <Image
                Source="dotnet_bot.png"
                HeightRequest="185"
                Aspect="AspectFit"
                SemanticProperties.Description="dot net bot in a race car number eight" />

            <CollectionView 
                ItemsSource="{Binding Jobs}"
                ItemTemplate="{StaticResource Common}" >
            </CollectionView>
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>
public partial class MainPage : ContentPage
{
    public MainPage() => InitializeComponent();
}

class JobsViewModel
{
    public ObservableCollection<Job> Jobs { get; } = new()
    {
        new Job { JobId = 101, Description = "Website Redesign", Client = "Acme Corp" },
        new Job { JobId = 102, Description = "Mobile App Development", Client = "Tech Innovations" },
        new Job { JobId = 103, Description = "Marketing Strategy", Client = "Global Enterprises" }
    };
}

Upvotes: 0

Related Questions