x:Bind DataTemplate 中事件的 ViewModel 方法

x:Bind ViewModel method to an Event inside DataTemplate

我基本上问的是与 this person 相同的问题,但在较新的 x:Bind.

的上下文中

ViewModels 的 DataContext 定义如下

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

所以每当我需要绑定某些东西时,我都会像这样明确地绑定到 ViewModel

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

但是这在模板中不起作用

<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>

阅读文档,我发现使用 Path 基本上应该将上下文重置为页面,但是这个 (x:Bind Path=ViewModel.PageResizeEvent 也不起作用。我仍然得到 Object reference not set to an instance of an object,这应该意味着它没有看到该方法(但为空)。

图片class:

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

并且在 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
}

我们这里有两个问题:

首先,尝试将事件直接绑定到事件处理程序委托

简单地说,这永远行不通。
在 MVVM 模式上处理事件的一种方法是使用 EventTrigger and ICommand.
它需要一个实现 ICommand 的 class。 This post 会帮助你如果不知道该怎么做。我会打电话给我 DelegateCommand

以下是我将如何分两步重构它:

1) 向虚拟机添加命令:

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

    public DelegateCommand PageResizedCommand { get; }

    private void OnPageResized()
    {  }
}

2) 使用 EventTrigger 和 InvokeCommandAction 将该命令绑定到 SizeChanged 事件。

<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",你说,"that didn't work!"

我知道!这是因为第二个问题,即 试图 x:Bind 一个不属于 DataTemplate class

的 属性

这个与 密切相关,所以我将从那里借用一些信息。

来自 MSDN,关于 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.

因此,当您执行 <ScrollViewer SizeChanged="{x:Bind ViewModel.PageResized}"> 时,您实际上是在 models:Image class 上搜索一个名为 属性 的 ViewModel,它是 DataTemplate 的 x:DataType。 class.

上不存在这样的 属性

在这里,我可以看到两个选项。 任选其一:

将该 ViewModel 添加为图像 class 上的 属性,并在 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;
    }
}

只有这样,之前的代码应该可以正常工作,无需更改任何其他内容。

放弃 x:Bind 并返回与 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>

这种方式违背了你提出问题的目的,但它确实有效,而且比前一种更容易实现。