【问题标题】:Binding to a single element inside a CompositeCollection绑定到 CompositeCollection 中的单个元素
【发布时间】:2009-12-01 14:50:07
【问题描述】:

我正在尝试生成用于在网络上浏览的服务器列表,以便生成如下所示的树形视图:

-Local Server
 - Endpoint 1
 - Endpoint 2
-Remote
 - <Double-click to add a server...>
 - Remote Server 1
   - Endpoint 1
   - Endpoint 2
 - Remote Server 2
   - Endpoint 1
   - Endpoint 2

我的 ViewModel 如下所示:

...
public Server LocalServer;
public ObservableCollection<Server> RemoteServers;
...

那么,如何在 xaml 中构造列表并绑定到单个对象对象列表?我的想法可能完全错误,但我的大脑真正想做的事情是这样的:

<CompositeCollection>
  <SingleElement Content="{Binding LocalServer}"> 
  <!-- ^^ something along the lines of a ContentPresenter -->
  <TreeViewItem Header="Remote">
    <TreeViewItem.ItemsSource>
      <CompositeCollection>
        <TreeViewItem Header="&lt;Click to add...&gt;" />
        <CollectionContainer Collection="{Binding RemoteServers}" />
      </CompositeCollection>
    </TreeViewItem.ItemsSource>
  </TreeViewItem>
</CompositeCollection>

我觉得我必须缺少一个基本元素,这使我无法在这里指定我想要的东西。该单项有子项。我确实尝试过使用 ContentPresenter,但无论出于何种原因,即使它选择了 HierarchicalDataTemplate 以正确显示标题,它也无法展开。


更新

所以现在,我在视图模型上公开了一个属性,该属性将单个元素包装在一个集合中,以便CollectionContainer 可以绑定到它。不过,我真的很想听听人们对如何做到这一点的想法。这似乎非常基础。

【问题讨论】:

  • 在找到这个 SO 问题之前,我也试图弄清楚这一点,并最终做了同样的事情。在我的视图模型中添加了一个集合属性。

标签: wpf xaml mvvm treeview


【解决方案1】:

我发布了一个与您的 CompositeCollections 非常相似的问题:Why is CompositeCollection not Freezable?

这显然是 WPF 中的一个错误,信不信由你。这是一位 MS 员工的帖子,他承认了这一点:http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/b15cbd9d-95aa-47c6-8068-7ae9f7dca88a

CompositeCollection 不可冻结,但应该可以。这使得将非静态元素组合到一个集合中变得困难。这是很多事情的常见场景。例如,组合框顶部的“Select One”元素填充其他数据绑定对象会很好,但您不能以声明方式进行。

无论如何,很抱歉这不是一个答案,但希望它可以帮助您了解为什么它没有按照您的想法工作。

【讨论】:

  • 这实际上不是我要解决的问题。对于您描述的情况,我通常使用 CollectionViewSource 来托管动态元素。然后,CollectionViewSource 很好地放置在 CollectionContainer 旁边的静态元素旁边,并且每个人都可以很好地协同工作。 -- 我在这里遇到的问题是如何将视图模型中的单个元素直接动态绑定到 CompositeCollection 中。不过,您肯定会得到我让 CompositeCollection 可冻结的签名。
  • @MojoFilter:CollectionContainer 怎么可能包含CollectionViewSource?似乎无法通过 Collection 属性实现。
  • @O.R.Mapper:这正是你的做法。在大多数情况下,您会使用与CollectionViewSource 的绑定作为Collection 属性值的静态资源。
【解决方案2】:

您不能只从您的 ViewModel 中公开一个树可以绑定到的新集合吗?

类似:

public Server LocalServer;
public ObservableCollection<Server> RemoteServers;

public IEnumerable ServerTree { return new[] { LocalServer, RemoteServers } }

毕竟你的 ViewModel 是一个 ViewModel。它应该准确地暴露视图所需的内容。

【讨论】:

  • 我可以,但是视图需要注入一个项目的用户界面概念,以便在树本身内单击以添加新的远程节点。这在很大程度上是一个视图概念而不是视图模型概念,为了保持单元测试的可行性和简单性,它确实需要以这种方式分离。将本地服务器包装在一个集合中确实可以解决问题,但我真的很想了解如何绑定到该单个元素。
【解决方案3】:

最后,仅仅过了几年,我的 WPF 技能就足以解决这个问题了 ;)

这是SingleElement,就像您在问题中概述的那样。它是通过继承 CollectionContainer 并将绑定元素放入集合中来实现的。通过注册更改处理程序,我们甚至可以在绑定更改时更新 CollectionContainer。对于原始的 CollectionProperty,我们指定了一个强制处理程序来防止我们类的用户弄乱集合属性,如果您想改进保护,您可以使用自定义集合而不是 ObservableCollection。作为奖励,我展示了如何使用占位符值使 SingleElement 消失,尽管从技术上讲这更像是一个“OptionalSingleElement”。

public class SingleElement : CollectionContainer
{
    public static readonly object EmptyContent = new object();

    public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(
        "Content", typeof(object), typeof(SingleElement), new FrameworkPropertyMetadata(EmptyContent, HandleContentChanged));

    static SingleElement()
    {
        CollectionProperty.OverrideMetadata(typeof(SingleElement), new FrameworkPropertyMetadata { CoerceValueCallback = CoerceCollection });
    }

    private static object CoerceCollection(DependencyObject d, object baseValue)
    {
        return ((SingleElement)d)._content;
    }

    private static void HandleContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var content = ((SingleElement)d)._content;

        if (e.OldValue == EmptyContent && e.NewValue != EmptyContent)
            content.Add(e.NewValue);
        else if (e.OldValue != EmptyContent && e.NewValue == EmptyContent)
            content.RemoveAt(0);
        else // (e.OldValue != EmptyContent && e.NewValue != EmptyContent)
            content[0] = e.NewValue;
    }

    private ObservableCollection<object> _content;

    public SingleElement()
    {
        _content = new ObservableCollection<object>();
        CoerceValue(CollectionProperty);
    }

    public object Content
    {
        get { return GetValue(ContentProperty); }
        set { SetValue(ContentProperty, value); }
    }
}

您可以完全按照您在问题中所说的那样使用它,除了您必须针对 CompositeCollection 中缺少 DataContext 进行调整:

<TreeView x:Name="wTree">
    <TreeView.Resources>
        <CompositeCollection x:Key="Items">
            <local:SingleElement Content="{Binding DataContext.LocalServer, Source={x:Reference wTree}}"/>
            <TreeViewItem Header="Remote">
                <TreeViewItem.ItemsSource>
                    <CompositeCollection>
                        <TreeViewItem Header="&lt;Click to add ...&gt;"/>
                        <CollectionContainer Collection="{Binding DataContext.RemoteServers, Source={x:Reference wTree}}"/>
                    </CompositeCollection>
                </TreeViewItem.ItemsSource>
            </TreeViewItem>
        </CompositeCollection>
    </TreeView.Resources>
    <TreeView.ItemsSource>
        <StaticResource ResourceKey="Items"/>
    </TreeView.ItemsSource>
</TreeView>

【讨论】:

    【解决方案4】:

    嗯,这是我能达到的最接近您的要求。所有的功能都没有包含在一个 TreeView 中,也没有绑定到一个复合集合,但这可能是你我之间的秘密;)

    <Window x:Class="CompositeCollectionSpike.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300"
    xmlns:local="clr-namespace:CompositeCollectionSpike">
    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="TreeView">
                <Setter Property="BorderThickness" Value="0"/>
            </Style>
            <HierarchicalDataTemplate DataType="{x:Type local:Server}"
                                      ItemsSource="{Binding EndPoints}">
                <Label Content="{Binding Name}"/>
            </HierarchicalDataTemplate>
        </StackPanel.Resources>
        <TreeView ItemsSource="{Binding LocalServer}"/>
        <TreeViewItem DataContext="{Binding RemoteServers}"
                      Header="{Binding Description}">
            <StackPanel>
                <Button Click="Button_Click">Add Remote Server</Button>
                <TreeView ItemsSource="{Binding}"/>
            </StackPanel>
        </TreeViewItem>
    </StackPanel>
    

    using System.Collections.ObjectModel;
    using System.Windows;
    
    namespace CompositeCollectionSpike
    {
        public partial class Window1 : Window
        {
            private ViewModel viewModel;
            public Window1()
            {
                InitializeComponent();
                viewModel = new ViewModel
                                {
                                    LocalServer =new ServerCollection{new Server()},
                                    RemoteServers =
                                        new ServerCollection("Remote Servers") {new Server(),
                                            new Server(), new Server()},
                                };
                DataContext = viewModel;
            }
    
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                viewModel.LaunchAddRemoteServerDialog();
            }
        }
    
        public class ViewModel:DependencyObject
        {
            public ServerCollection LocalServer { get; set; }
            public ServerCollection RemoteServers { get; set; }
    
            public void LaunchAddRemoteServerDialog()
            {}
        }
    
        public class ServerCollection:ObservableCollection<Server>
        {
            public ServerCollection(){}
    
            public ServerCollection(string description)
            {
                Description = description;
            }
            public string Description { get; set; }
        }
    
        public class Server
        {
            public static int EndpointCounter;
            public static int ServerCounter;
            public Server()
            {
                Name = "Server"+ ++ServerCounter;
                EndPoints=new ObservableCollection<string>();
                for (int i = 0; i < 2; i++)
                {
                    EndPoints.Add("Endpoint"+ ++EndpointCounter);
                }
            }
            public string Name { get; set; }
            public ObservableCollection<string> EndPoints { get; set; }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2017-02-28
      • 1970-01-01
      • 1970-01-01
      • 2020-10-28
      • 1970-01-01
      • 1970-01-01
      • 2010-09-23
      • 1970-01-01
      相关资源
      最近更新 更多