3

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
}
Gabriel Rainha
  • 1,713
  • 1
  • 21
  • 34
rancor1223
  • 356
  • 2
  • 16
  • Does that models:Image class have a ViewModel property? If it doesn't and you're trying to refer to that same ViewModel you bound to `FlipView.ItemsSource`, then you'll not be able to access it that way, as the DataContext of the DataTemplate is now a models:Image object. – Gabriel Rainha Nov 23 '16 at 20:18
  • It does. The code works if I take out the `SizeChanged="{x:Bind ViewModel.PageResized}"`. But I need to be able to adjust the size of the image in the size of the ScrollView, which requires me to access ViewModel properties from within the template. – rancor1223 Nov 23 '16 at 20:47
  • Would you mind adding the code for that Image class? – Gabriel Rainha Nov 23 '16 at 20:49
  • have you been trying with old good Binding instead of x:Bind? – RTDev Nov 23 '16 at 20:51
  • There is another problem: even if that Image class do have a ViewModel property, you cannot bind two events like that. You should be using [EventTrigger and Command](http://stackoverflow.com/a/4897897/4905310) instead. But first, add the code for the Image class so we can answer that properly. – Gabriel Rainha Nov 23 '16 at 21:03
  • Added the class. I suppose Command would get around the problem since it's not defined within the Template though. I will give that a try. – rancor1223 Nov 23 '16 at 21:10
  • @RTDev UWP doesn't support `x:Type` which I think `Binding` would need in this case. – rancor1223 Nov 23 '16 at 21:11
  • You do realize that neither your VM and your Image class have that PageResized, right? Or is it hidden on a VM's base class? – Gabriel Rainha Nov 23 '16 at 21:15
  • I didn't mention it, because it's just an empty method. It's in the VM. I added it it to the description. – rancor1223 Nov 23 '16 at 21:20

1 Answers1

4

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.

Community
  • 1
  • 1
Gabriel Rainha
  • 1,713
  • 1
  • 21
  • 34
  • That did the trick. And introduced other problem - inability to get the event arguments. However, it made me realize I was just making my life harder by reading the dimensions of the template children, when I could just as well (and much more easily) read the FlipView dimensions. And that Command example will surely be useful in the future too. Thanks! – rancor1223 Nov 24 '16 at 00:42
  • @rancor1223 Well, that event args is an old problem for MVVM. So, [I'll just leave this here](http://stackoverflow.com/questions/6205472/mvvm-passing-eventargs-as-command-parameter) – Gabriel Rainha Nov 24 '16 at 01:09