将ViewModel方法绑定到DataTemplate内部的事件

3

我基本上是在问与这个人相同的问题,但是在新的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的错误,这意味着它没有找到该方法(而是一个null)。

图片类:

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
}

如果我删掉 SizeChanged="{x:Bind ViewModel.PageResized}" 这一行,代码就能正常工作。但我需要能够调整图像的大小以适应 ScrollView 的大小,这需要我从模板中访问 ViewModel 属性。 - rancor1223
你介意加上那个Image类的代码吗? - Gabriel Rainha
还有一个问题:即使Image类确实有ViewModel属性,你也不能像那样绑定两个事件。你应该使用EventTrigger和Command。但首先,添加Image类的代码,这样我们才能正确回答。 - Gabriel Rainha
@RTDev UWP不支持x:Type,在这种情况下,我认为Binding会需要它。 - rancor1223
我没有提到它,因为它只是一个空方法。它在虚拟机中。我将其添加到了描述中。 - rancor1223
显示剩余4条评论
1个回答

4
我们有两个问题:
第一个问题是,试图直接将事件绑定到事件处理程序委托中,这样做根本行不通。
一种处理MVVM模式下事件的方法是使用EventTrigger和ICommand。它需要一个实现ICommand的类。如果您不知道如何操作,此帖子将为您提供帮助。我将称其为DelegateCommand
以下是我将如何进行的两个步骤:
1)在VM中添加一个命令:
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>

"但是,加布里埃尔",你说,"那不起作用!"

我知道!这是第二个问题,即尝试将不属于DataTemplate类的属性x:Bind

这个问题与this question密切相关,因此我会从那里借鉴一些信息。

来自MSDN关于DataTemplate和x:Bind的说明

在DataTemplate内部(无论是作为项目模板、内容模板还是标题模板使用),Path的值都不是在页面的上下文中解释的,而是在被模板化的数据对象的上下文中解释的。因此,为了使其绑定能够在编译时进行验证(并为它们生成高效的代码),DataTemplate需要使用x:DataType声明其数据对象的类型。

所以,当你执行<ScrollViewer SizeChanged="{x:Bind ViewModel.PageResized}">时,实际上是在那个models:Image类中寻找名为ViewModel的属性,它是DataTemplate的x:DataType。但是该类上不存在这样的属性。
在这里,我可以看到两个选项。请选择其中一个:
1. 在Image类上添加ViewModel作为属性,并在VM中填充它。 2. 更改绑定以使用正确的ViewModel。
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>

这种方法有点违背你的问题目的,但它有效且比之前的方法更容易实现。

成功了。但引入了另一个问题——无法获取事件参数。不过,这让我意识到,通过读取模板子元素的尺寸让我变得更加困难,而我同样可以(并且更容易地)读取FlipView的尺寸。而且那个命令示例将来肯定也会很有用。谢谢! - rancor1223
@rancor1223 嗯,那个事件参数对于MVVM来说是一个老问题。因此,我会在这里留下这个链接 - Gabriel Rainha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接