rancor1223 rancor1223 - 13 days ago 8
C# Question

x:Bind ViewModel method to an Event inside DataTemplate

I'm basically asking the same question as this person, but in the context of the newer

x:Bind
.

ViewModels' DataContext is defined like so

<Page.DataContext>
<vm:ChapterPageViewModel x:Name="ViewModel" />
</Page.DataContext>


So whenever I need to bind something I do it explicitely to the ViewModel like so

ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}"


However that doesn't work within templates

<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}">
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:Image">
<ScrollViewer SizeChanged="{x:Bind ViewModel.PageResized}"> <-- this here is the culprit
<Image Source="{x:Bind url}"/>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>


Reading the documentation, I found that using
Path
should basically reset the context to the page, but this (
x:Bind Path=ViewModel.PageResizeEvent
didn't work either. I'm still getting
Object reference not set to an instance of an object
, which should mean that it doesn't see the method (but a null).

Image class:

public class Image {
public int page { get; set; }
public string url { get; set; }
public int width { get; set; }
public int heigth { get; set; }
}


And in the ChapterPageViewModel

private List<Image> _pageList;
public List<Image> pageList {
get { return _pageList; }
set { Set(ref _pageList, value); }
}

public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode,
IDictionary<string, object> suspensionState)
{
Initialize();

await Task.CompletedTask;
}

private async void Initialize()
{
pageList = await ComicChapterGet.GetAsync(_chapterId);
}

public void PageResized(object sender, SizeChangedEventArgs e)
{
//resizing logic happens here
}

Answer

We have two problems here:

First, trying to directly bind an event to a event handler delegate

That will never work, simply put.
One way to handle an event on MVVM pattern is by using EventTrigger and ICommand.
It requires a class that implements ICommand. This post will help you if don't know how to do it. I'll call mine DelegateCommand.

Here's how I would refactor it in two steps:

1) Add a Command to the VM:

public class ChapterPageViewModel
{
    public ChapterPageViewModel()
    {
        this.PageResizedCommand = new DelegateCommand(OnPageResized);
    }

    public DelegateCommand PageResizedCommand { get; }

    private void OnPageResized()
    {  }
}

2) Bind that Command to the SizeChanged event with EventTrigger and InvokeCommandAction.

<Page (...)
  xmlns:i="using:Microsoft.Xaml.Interactivity"
  xmlns:core="using:Microsoft.Xaml.Interactions.Core">
    (...)
    <FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}" >
        <FlipView.ItemTemplate>
            <DataTemplate x:DataType="models:Image">
                <ScrollViewer>
                    <i:Interaction.Behaviors>
                        <core:EventTriggerBehavior EventName="SizeChanged">
                            <core:InvokeCommandAction 
                              Command="{x:Bind ViewModel.PageResizedCommand }" />
                        </core:EventTriggerBehavior>
                    </i:Interaction.Behaviors>

                    <Image Source="{x:Bind url}"/>
                </ScrollViewer>
            </DataTemplate>
        </FlipView.ItemTemplate>
    </FlipView>
</Page>

"But Gabriel", you say, "that didn't work!"

I know! And that's because of the second problem, which is trying to x:Bind a property that does not belong to the DataTemplate class

This one is closely related to this question, so I´ll borrow some info from there.

From MSDN, regarding DataTemplate and x:Bind

Inside a DataTemplate (whether used as an item template, a content template, or a header template), the value of Path is not interpreted in the context of the page, but in the context of the data object being templated. So that its bindings can be validated (and efficient code generated for them) at compile-time, a DataTemplate needs to declare the type of its data object using x:DataType.

So, when you do <ScrollViewer SizeChanged="{x:Bind ViewModel.PageResized}">, you're actually searching for a property named ViewModel on the that models:Image class, which is the DataTemplate's x:DataType. And such a property does not exist on that class.

Here, I can see two options. Choose one of them:

Add that ViewModel as a property on the Image class, and fill it up on the VM.

public class Image {
    (...)
    public ChapterPageViewModel ViewModel { get; set; }
}

public class ChapterPageViewModel
{
    (...)
    private async void Initialize() {
        pageList = await ComicChapterGet.GetAsync(_chapterId);
        foreach(Image img in pageList)
            img.ViewModel = this;
    }
}

With only this, that previous code should work with no need to change anything else.

Drop that x:Bind and go back to good ol'Binding with ElementName.

<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}" x:Name="flipView">
    <FlipView.ItemTemplate>
        <DataTemplate x:DataType="models:Image">
            <ScrollViewer> 
                <i:Interaction.Behaviors>
                    <core:EventTriggerBehavior EventName="SizeChanged">
                        <core:InvokeCommandAction 
                          Command="{Binding DataContext.PageResizedCommand
                            , ElementName=flipView}" />
                    </core:EventTriggerBehavior>
                </i:Interaction.Behaviors>

                <Image Source="{x:Bind url}"/>
            </ScrollViewer>
        </DataTemplate>
    </FlipView.ItemTemplate>
</FlipView>

This one kind of defeat the purpose of your question, but it does work and it's easier to pull off then the previous one.