如何在 UWP 的 ItemsControl 中将 ItemTemplate 的 Binding 设置为宿主容器?

6

如果我有一个任意的ItemsControl,比如ListView,我想在ItemsTemplate内部设置与托管容器的绑定,该怎么做呢?例如,在WPF中,我们可以使用这个代码实现:

{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}

<ListView.ItemTemplate>
    <DataTemplate>
        <SomeControl Property="{Binding Path=TargetProperty, RelativeSouce={RelativeSource FindAncestor, AncestorType={x:Type MyContainer}}}" />
    </DataTemplate>
<ListView.ItemTemplate>

在这个例子中(针对WPF),Binding将在SomeControlPropertyListViewItemTargetProperty之间设置(隐式地,因为它将由ListView动态生成,来承载其每个项目)。
我们如何在UWP中实现相同的效果呢?
我想要一个MVVM友好的解决方案。也许使用附加属性或交互行为?

1
问题在于ListViewItem是隐式创建的,而且不容易获取。如果您对此有任何想法,我会很感兴趣。 - Petter Hesselberg
我在想是否能创建一个派生的ListView类,其中可以在DataTemplate中包含ListViewItem,这样就不会生成新容器,而是使用那个显式的ListViewItem。可以使用ElementName绑定。我是不是太有想象力了? - SuperJMN
可能。如果您尝试走这条路线,我会对结果感到好奇。(这可能会成为这个问题的另一个答案。) - Petter Hesselberg
1
你是否试图在DataTemplate内部绑定到外部的内容?想要了解问题是否围绕从ItemTemplate内部绑定到主模型属性。 - loopedcode
你究竟想要实现什么目标? - Naweed Akram
显示剩余13条评论
3个回答

2

当选择改变时,搜索可视树以查找与所选/取消选定项目对应的DataContext的单选按钮。一旦找到,您可以随意勾选/取消选中。

我有一个玩具模型对象,长这样:

public class Data
{
    public string Name { get; set; }
}

我的页面名为self,包含以下集合属性:

public Data[] Data { get; set; } =
    {
        new Data { Name = "One" },
        new Data { Name = "Two" },
        new Data { Name = "Three" },
    };

列表视图,绑定到上面的集合:
<ListView
    ItemsSource="{Binding Data, ElementName=self}"
    SelectionChanged="OnSelectionChanged">
    <ListView.ItemTemplate>
        <DataTemplate>
            <RadioButton Content="{Binding Name}" />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
SelectionChanged事件处理程序:
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ListView lv = sender as ListView;

    var removed = FindRadioButtonWithDataContext(lv, e.RemovedItems.FirstOrDefault());
    if (removed != null)
    {
        removed.IsChecked = false;
    }

    var added = FindRadioButtonWithDataContext(lv, e.AddedItems.FirstOrDefault());
    if (added != null)
    {
        added.IsChecked = true;
    }
}

找到与我们的Data实例匹配的DataContext的单选按钮:

public static RadioButton FindRadioButtonWithDataContext(
    DependencyObject parent,
    object data)
{
    if (parent != null)
    {
        int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < childrenCount; i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(parent, i);
            ListViewItem lv = child as ListViewItem;
            if (lv != null)
            {
                RadioButton rb = FindVisualChild<RadioButton>(child);
                if (rb?.DataContext == data)
                {
                    return rb;
                }
            }

            RadioButton childOfChild = FindRadioButtonWithDataContext(child, data);
            if (childOfChild != null)
            {
                return childOfChild;
            }
        }
    }

    return null;
}

最后,一个查找特定类型子元素的辅助方法:
public static T FindVisualChild<T>(
    DependencyObject parent)
    where T : DependencyObject
{
    if (parent != null)
    {
        int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < childrenCount; i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(parent, i);
            T candidate = child as T;
            if (candidate != null)
            {
                return candidate;
            }

            T childOfChild = FindVisualChild<T>(child);
            if (childOfChild != null)
            {
                return childOfChild;
            }
        }
    }

    return default(T);
}

结果:

enter image description here

如果给定的模型实例在列表中出现多次,则会发生错误。这将导致程序崩溃。

这是一种实现相同结果的不错方式,但它也有一些缺点:它不可重用,会增加大量样板代码,并且看起来像一个hack。他们为什么要删除WPF的最佳功能? - SuperJMN
1
呵。我在WPF中最喜欢的缺失部分是自定义标记扩展。虽然我相信有可能以更易用和可重复使用的方式打包我的解决方案,但我只是选择了从A到B最快的方式。 - Petter Hesselberg
1
我的最后一个辅助方法,例如,是我经常使用的实用程序。 - Petter Hesselberg
太棒了!我刚刚问了关于在UWP中创建自定义MarkupExtensions的问题(https://dev59.com/sp3ha4cB1Zd3GeqPVoNG)。真是个令人难以置信的打击。 - SuperJMN
1
我刚刚修改了代码,以消除模型类的依赖。这是对可重用性的轻微改进。 - Petter Hesselberg
抱歉,经过一年的时间,我决定取消接受的答案检查,因为这种解决方案远非MVVM友好,并且仅适用于此场景(选择项目)。如果我想绑定到容器的任意属性,你又会遇到麻烦。 - SuperJMN

1
注意:本答案基于WPF,对于UWP可能需要进行一些更改。
基本上有两种情况需要考虑:
1. 您有一个数据驱动的方面需要绑定到项目容器。 2. 您有一个仅用于视图的属性。
让我们假设针对这两种情况都进行了自定义的列表视图:
public class MyListView : ListView
{
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new DesignerItem();
    }
    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return item is DesignerItem;
    }
}
public class DesignerItem : ListViewItem
{
    public bool IsEditing
    {
        get { return (bool)GetValue(IsEditingProperty); }
        set { SetValue(IsEditingProperty, value); }
    }
    public static readonly DependencyProperty IsEditingProperty =
        DependencyProperty.Register("IsEditing", typeof(bool), typeof(DesignerItem));
}

在情况1中,您可以使用ItemContainerStyle将您的视图模型属性与绑定链接,然后在数据模板内绑定相同的属性。
class MyData
{
    public bool IsEditing { get; set; } // also need to implement INotifyPropertyChanged here!
}

XAML:
<local:MyListView ItemsSource="{Binding Source={StaticResource items}}">
    <local:MyListView.ItemContainerStyle>
        <Style TargetType="{x:Type local:DesignerItem}">
            <Setter Property="IsEditing" Value="{Binding IsEditing,Mode=TwoWay}"/>
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        </Style>
    </local:MyListView.ItemContainerStyle>
    <local:MyListView.ItemTemplate>
        <DataTemplate>
            <Border Background="Red" Margin="5" Padding="5">
                <CheckBox IsChecked="{Binding IsEditing}"/>
            </Border>
        </DataTemplate>
    </local:MyListView.ItemTemplate>
</local:MyListView>

在情况2中,似乎您并没有真正的数据驱动属性,因此,您属性的效果应该反映在控件(ControlTemplate)中。
以下示例中,基于IsEditing属性使工具栏可见。可以使用切换按钮来控制该属性,ItemTemplate作为工具栏和按钮旁的内部元素使用,它不知道IsEditing状态:
<local:MyListView ItemsSource="{Binding Source={StaticResource items}}">
    <local:MyListView.ItemContainerStyle>
        <Style TargetType="{x:Type local:DesignerItem}">
            <Setter Property="IsEditing" Value="{Binding IsEditing,Mode=TwoWay}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:DesignerItem}">
                        <DockPanel>
                            <ToggleButton DockPanel.Dock="Right" Margin="5" VerticalAlignment="Top" IsChecked="{Binding IsEditing,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}" Content="Edit"/>
                            <!--Toolbar is something control related, rather than data related-->
                            <ToolBar x:Name="MyToolBar" DockPanel.Dock="Top" Visibility="Collapsed">
                                <Button Content="Tool"/>
                            </ToolBar>
                            <ContentPresenter ContentSource="Content"/>
                        </DockPanel>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsEditing" Value="True">
                                <Setter TargetName="MyToolBar" Property="Visibility" Value="Visible"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </local:MyListView.ItemContainerStyle>
    <local:MyListView.ItemTemplate>
        <DataTemplate>
            <Border Background="Red" Margin="5" Padding="5">
                <TextBlock Text="Hello World"/>
            </Border>
        </DataTemplate>
    </local:MyListView.ItemTemplate>
</local:MyListView>

为了更好地控制模板,您可以选择使用 Blend 并从完整的 ListViewItem 模板开始创建控件模板,然后将您的更改编辑到其中。
如果您的 DesignerItem 通常具有特定的增强外观,请考虑在 Themes/Generic.xaml 中设计它,并使用适当的默认样式。
如评论所述,您可以为编辑模式提供单独的数据模板。要做到这一点,请向MyListViewDesignerItem添加属性,并使用MyListView.PrepareContainerForItemOverride(...)转移模板。
为了在不需要Setter.Value绑定的情况下应用模板,您可以基于IsEditingDesignerItem.ContentTemplate进行值强制。
public class MyListView : ListView
{
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new DesignerItem();
    }
    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return item is DesignerItem;
    }

    protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        base.PrepareContainerForItemOverride(element, item);
        var elem = element as DesignerItem;
        elem.ContentEditTemplate = ItemEditTemplate;
    }

    public DataTemplate ItemEditTemplate
    {
        get { return (DataTemplate)GetValue(ItemEditTemplateProperty); }
        set { SetValue(ItemEditTemplateProperty, value); }
    }
    public static readonly DependencyProperty ItemEditTemplateProperty =
        DependencyProperty.Register("ItemEditTemplate", typeof(DataTemplate), typeof(MyListView));
}

public class DesignerItem : ListViewItem
{
    static DesignerItem()
    {
        ContentTemplateProperty.OverrideMetadata(typeof(DesignerItem), new FrameworkPropertyMetadata(
            null, new CoerceValueCallback(CoerceContentTemplate)));
    }
    private static object CoerceContentTemplate(DependencyObject d, object baseValue)
    {
        var self = d as DesignerItem;
        if (self != null && self.IsEditing)
        {
            return self.ContentEditTemplate;
        }
        return baseValue;
    }

    private static void OnIsEditingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.CoerceValue(ContentTemplateProperty);
    }

    public bool IsEditing
    {
        get { return (bool)GetValue(IsEditingProperty); }
        set { SetValue(IsEditingProperty, value); }
    }
    public static readonly DependencyProperty IsEditingProperty =
        DependencyProperty.Register("IsEditing", typeof(bool), typeof(DesignerItem), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsEditingChanged)));

    public DataTemplate ContentEditTemplate
    {
        get { return (DataTemplate)GetValue(ContentEditTemplateProperty); }
        set { SetValue(ContentEditTemplateProperty, value); }
    }
    // Using a DependencyProperty as the backing store for ContentEditTemplate.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ContentEditTemplateProperty =
        DependencyProperty.Register("ContentEditTemplate", typeof(DataTemplate), typeof(DesignerItem));
}

注意,为了举例说明,我将通过使用一些触发器来激活“编辑”模式,方法是通过ListViewItem.IsSelected
<local:MyListView ItemsSource="{Binding Source={StaticResource items}}">
    <local:MyListView.ItemContainerStyle>
        <Style TargetType="{x:Type local:DesignerItem}">
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="IsEditing" Value="True"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </local:MyListView.ItemContainerStyle>
    <local:MyListView.ItemTemplate>
        <DataTemplate>
            <Border Background="Red" Margin="5" Padding="5">
                <TextBlock Text="Hello World"/>
            </Border>
        </DataTemplate>
    </local:MyListView.ItemTemplate>
    <local:MyListView.ItemEditTemplate>
        <DataTemplate>
            <Border Background="Green" Margin="5" Padding="5">
                <TextBlock Text="Hello World"/>
            </Border>
        </DataTemplate>
    </local:MyListView.ItemEditTemplate>
</local:MyListView>

预期行为:所选项目变为可编辑状态,获取local:MyListView.ItemEditTemplate(绿色)而不是默认模板(红色)。


1
当然,使用自定义的ItemsControl和ItemContainer,也可以在ItemsControl中添加更多的模板,例如额外的local:MyListView.ItemEditTemplate,并使ItemContainer在编辑时切换模板。 - grek40
我对你所说的很感兴趣。我该如何在常规模板和编辑模板之间切换?我认为你不能只是更改ItemTemplate(热插拔)。你觉得呢? - SuperJMN
顺便说一句,我认为你不能将绑定作为 UWP 中 Setter 的值 :( https://dev59.com/f1wX5IYBdhLWcg3wpgum - SuperJMN
@SuperJMN 我不确定,但它只是在谈论样式,所以有些幸运的话,ControlTemplate.Triggers-Setter.Value绑定仍将起作用 :) - grek40
@SuperJMN 哈哈 :) 请查看我的编辑,以获取处理活动内容模板的另一种方法(我重新设计了答案下面的部分)。 - grek40

1

如果您想在视图模型项类中拥有一个IsSelected属性,您可以创建一个派生的ListView,将其ListViewItems与视图模型属性建立绑定:

public class MyListView : ListView
{
    public string ItemIsSelectedPropertyName { get; set; } = "IsSelected";

    protected override void PrepareContainerForItemOverride(
        DependencyObject element, object item)
    {
        base.PrepareContainerForItemOverride(element, item);

        BindingOperations.SetBinding(element,
            ListViewItem.IsSelectedProperty,
            new Binding
            {
                Path = new PropertyPath(ItemIsSelectedPropertyName),
                Source = item,
                Mode = BindingMode.TwoWay
            });
    }
}

您现在可以将ListView的ItemTemplate中的RadioButton的IsChecked属性绑定到相同的视图模型属性上。请注意保留html标签,不进行解释。
<local:MyListView ItemsSource="{Binding DataItems}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <RadioButton Content="{Binding Content}"
                         IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
        </DataTemplate>
    </ListView.ItemTemplate>
</local:MyListView>

在上面的例子中,数据项类还具有Content属性。显然,数据项类的IsSelected属性必须触发一个PropertyChanged事件。

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