UWP数据绑定:如何将按钮命令设置为DataTemplate内父级DataContext

7

需求简述:我需要在DataTemplate内部使用来自ViewModel的DataContext方法来触发按钮的命令。

问题简述:模板化按钮的命令似乎只能绑定到项目本身的datacontext。WPF和Windows 8.1应用程序用于遍历可视树的语法似乎不起作用,包括ElementName和Ancestor binding。我非常希望我的按钮命令不要位于MODEL中。

附注:这是采用MVVM设计方法构建的。

下面的代码生成VIEW上的项目列表。该列表是每个列表项的一个按钮。

            <ItemsControl x:Name="listView" Tag="listOfStories" Grid.Row="0" Grid.Column="1"
            ItemsSource="{x:Bind ViewModel.ListOfStories}"
            ItemTemplate="{StaticResource storyTemplate}"
            Background="Transparent"
            IsRightTapEnabled="False"
            IsHoldingEnabled="False"
            IsDoubleTapEnabled="False"
                 />

在同一个VIEW的页面资源内,我创建了一个DataTemplate,其中包含有问题的按钮。我去掉了按钮内部大部分格式,例如文本,以便于在此处更容易阅读代码。关于按钮的所有内容都可以正常工作,除了列出的问题,即命令绑定。

<Page.Resources>
        <DataTemplate x:Name="storyTemplate" x:DataType="m:Story">
            <Button
                Margin="0,6,0,0"
                Width="{Binding ColumnDefinitions[1].ActualWidth, ElementName=storyGrid, Mode=OneWay}"
                HorizontalContentAlignment="Stretch"
                CommandParameter="{Binding DataContext, ElementName=Page}"
                Command="{Binding Source={StaticResource Locator}}">

                <StackPanel HorizontalAlignment="Stretch" >
                    <TextBlock Text="{x:Bind StoryTitle, Mode=OneWay}"
                        FontSize="30"
                        TextTrimming="WordEllipsis"
                        TextAlignment="Left"/>
                </StackPanel>
            </Button>
        </DataTemplate>
    </Page.Resources>

因为这是一个DataTemplate,所以DataContext已经被设置为组成列表的各个项(MODEL)。我需要做的是选择列表本身的DataContext(VIEWMODEL),以便我可以访问导航命令。
如果您对VIEW页面的代码有兴趣,请参见下面。
    public sealed partial class ChooseStoryToPlay_View : Page
    {
    public ChooseStoryToPlay_View()
    {
        this.InitializeComponent();
        this.DataContextChanged += (s, e) => { ViewModel = DataContext as ChooseStoryToPlay_ViewModel; };
    }
    public ChooseStoryToPlay_ViewModel ViewModel { get; set; }
}

我尝试使用ElementName进行设置,还有其他方法,但都失败了。当输入ElementName时,Intellisense会检测到“storyTemplate”作为选项,这是第一个代码块中显示的DataTemplate的名称。

我认为我的问题可能不是独一无二的,但我很难找到UWP的解决方案。如果这是一个简单的问题,那么请允许我提前道歉,但我已经花了将近两天的时间研究答案,但似乎没有任何适用于UWP的答案。

谢谢大家!


我从未尝试过UWP,但在其他基于XAML的框架中,通常使用RelativeSource来处理。如果这不起作用,则可能是由于DataTemplate位于资源中(虽然这很可疑),或者UWP XAML具有其自己的特殊性。 - Eugene Podskal
是的先生,这就是导致我如此沮丧的原因。我做的第一件事是使用RelativeSource祖先绑定,就像我在过去的平台上所做的那样,但这次它没有起作用。谢谢。 - RAB
3个回答

8
你正在使用哪个MVVM工具包(如果有的话)?在MVVM Light中,你可以通过与设置视图DataContext相同的方式从DataTemplate获取ViewModel:

<DataTemplate x:Key="SomeTemplate">
    <Button Command="{Binding Main.MyCommand, Source={StaticResource ViewModelLocator}}"/>
</DataTemplate>

这会实例化一个新的、断开连接的 ViewModel 吗? 谢谢您的回答! - RAB
不会的。在您的ViewModelLocator Main中,只需从默认ServiceLocator获取MainViewModel的实例,该实例返回单例即可。更多信息 - Andrei Ashikhmin
拍着额头 我现在感觉像个彻底的白痴。Syn,请允许我最诚挚地感谢你。我因为看这段代码而眼睛疲惫不堪,但我认为你的代码会起作用! - RAB
确保您的定位器设置正确,它将像魔法般正常工作。很高兴能够帮助您。 - Andrei Ashikhmin
顺便说一句,我喜欢你的头像。这是指挥官谢泼德,我批准你的回答。 - RAB
了不起的工作。对我来说,它是 Source={StaticResource Locator},以防其他人使用 MVVMLight 示例。 - Clinton Ward

3
很遗憾,UWP中没有祖先绑定。这使得像你这样的情况更加难以实现。唯一能想到的方法是在你的页面上为ViewModel创建一个DependencyProperty。
public ChooseStoryToPlay_ViewModel ViewModel
{
    get { return (ChooseStoryToPlay_ViewModel)GetValue(ViewModelProperty); }
    set { SetValue(ViewModelProperty, value); }
}

public static readonly DependencyProperty ViewModelProperty =
    DependencyProperty.Register("ViewModel", typeof(ChooseStoryToPlay_ViewModel), typeof(MainPage), new PropertyMetadata(0));

现在,您可以从数据模板中绑定到它:
<DataTemplate x:Name="storyTemplate" x:DataType="local:Story">
    <Button
        Margin="0,6,0,0"
        Width="{Binding ColumnDefinitions[1].ActualWidth, ElementName=storyGrid, Mode=OneWay}"
        HorizontalContentAlignment="Stretch"
        CommandParameter="{x:Bind Page}"
        Command="{Binding ViewModel.NavigateCommand, ElementName=Page}">

        <StackPanel HorizontalAlignment="Stretch" >
            <TextBlock Text="{x:Bind StoryTitle, Mode=OneWay}"
                FontSize="30"
                TextTrimming="WordEllipsis"
                TextAlignment="Left"/>
        </StackPanel>
    </Button>
</DataTemplate>

一些需要注意的事项:
  • In CommandParameter I assumed that in your Story class there is a Page property that you want to pass as a parameter to your command. You can bind to any other property of Story class here or the class itself.
  • You have to set the name of your page to Page (x:name="Page"), so that you can reference it using ElementName in the data template.
  • I assumed that the command you're calling on the ViewModel is named NavigateCommand and accepts a parameter of the same type as the property bound to CommandParameter:

    public ICommand NavigateCommand { get; } = 
        new RelayCommand<string>(name => Debug.WriteLine(name));
    
我希望这能对你有所帮助,并适用于你的情况。

太棒了。非常感谢您解决了祖先绑定在UWP中是否可用的问题。在无法将视图模型命令绑定时,我已经基本上决定有两个选择。一是在MODEL上创建一个命令,并将故事作为参数传递,但我不愿意这样做...或者在VIEW后端的按钮上应用点击参数。小小的抱怨一下,但我觉得微软吹嘘为程序未来的平台却剥夺了这种功能是难以理解的。再次感谢,Damir! - RAB
如果DataContext在ResourceDictionary中,这并没有帮助。有人找到了适当的解决方案吗? - Michael Puckett II

0

有几种方法可以做到这一点。但我认为命令更改更好...

例如,您有一个带有某些项模板的(网格、列表)视图,如下所示:

                    <GridView.ItemTemplate>
                        <DataTemplate>

                            <Grid
                                    x:Name="gdVehicleImage"
                                    Height="140"
                                    Width="140"
                                    Background="Gray"
                                    Margin="2"

                                >

                           </Grid>

                 </GridView.ItemTemplate>

你想要创建一个命令,例如针对FlyoutMenu... 但是该命令在ViewModel中而不是GridView.SelectedItem中...

你可以做的是...

                        <Grid
                                    x:Name="gdVehicleImage"
                                    Height="140"
                                    Width="140"
                                    Background="Gray"
                                    Margin="2"

                                >

                                <FlyoutBase.AttachedFlyout>
                                    <MenuFlyout
                                            Opened="MenuFlyout_Opened"
                                            Closed="MenuFlyout_Closed"
                                        >

                                        <MenuFlyout.MenuFlyoutPresenterStyle>
                                            <Style TargetType="MenuFlyoutPresenter">
                                                <Setter Property="Background" Value="DarkCyan"/>
                                                <Setter Property="Foreground" Value="White"/>
                                            </Style>
                                        </MenuFlyout.MenuFlyoutPresenterStyle>

                                        <MenuFlyoutItem 

                                                Loaded="mfiSetAsDefaultPic_Loaded" 
                                                CommandParameter="{Binding}"
                                                />
                                        <MenuFlyoutItem 

                                                Loaded="mfiDeletePic_Loaded" 
                                                CommandParameter="{Binding}"
                                                />

                                    </MenuFlyout>
                                </FlyoutBase.AttachedFlyout>


             </Grid>

而在已加载的事件中:

    private void mfiDeletePic_Loaded(object sender, RoutedEventArgs e)
    {
        var m = (MenuFlyoutItem)sender;

        if (m != null)
        {

            m.Command = Vm.DeleteImageCommand;
            //Vm is the ViewModel instance...
        }
    }

不是完全美观的... 但你不会像这样破坏mvvm模式...


非常感谢你抽出时间阅读我的问题并编写代码。但恐怕它对我的应用程序帮助不大。我只需要触发一个按钮命令来激活 ViewModel 上的备用命令以导航到另一个页面,同时传递任何参数。但还是非常感谢你! - RAB
是的,就像弹出菜单一样考虑你的按钮...将按钮命令放在加载事件中,但保持绑定的CommandParameter...然后您将拥有带参数的命令以导航到另一页:D - Breno Santos

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