7

I am trying to produce a list of servers for browsing on a network such that it produces a tree view which looks like this:

-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

My ViewModel looks like this:

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

So, how does one go about constructing the list in xaml with a binding to a single object and a list of objects? I might be thinking about it completely the wrong way, but what my brain really wants to be able to do is something like this:

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

I feel like there must be a fundamental element I'm missing which keeps me from being able to specify what I want here. That single item has children. I did try using a ContentPresenter, but for whatever reason, it was not expandable even though it picked up the HierarchicalDataTemplate to display the title correctly.


Update

So for now, I've exposed a property on the view model that wraps the single element in a collection so that a CollectionContainer may bind to it. I would really like to hear folks' ideas on how to do this, though. It seems awfully fundamental.

MojoFilter
  • 12,256
  • 14
  • 53
  • 61
  • 1
    Before finding this SO question, I was trying to figure this out too, and ended up doing the same thing.. added a collection property in my view model. – Mike Schenk Apr 14 '10 at 21:55
  • You can bind one element to a CollectionContainer through the simplest "Object To IEnumerable" converter. I showed an example implementation here: https://stackoverflow.com/a/73872792/13349759 – EldHasp Sep 27 '22 at 19:39

4 Answers4

2

I posted a question very similar to yours regarding CompositeCollections: Why is CompositeCollection not Freezable?

This is apparently a bug in WPF, believe it or not. Here's a post by an MS employee admitting as much: http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/b15cbd9d-95aa-47c6-8068-7ae9f7dca88a

The CompositeCollection is not freezable, but should be. This makes it difficult to combine nonstatic elements into one collection. It's a common scenario for a lot of things. For example, a "Select One" element at the top of a combobox filled with other databound objects would be nice, but you can't do it declaratively.

Anyway, I'm sorry this is not an answer, but hopefully it helps you see why this isn't working how you thought it should.

Community
  • 1
  • 1
Anderson Imes
  • 25,500
  • 4
  • 67
  • 82
  • 2
    That's actually not the problem I'm trying to work through. For the situation you're describing, I generally use a CollectionViewSource to host the dynamic elements. That CollectionViewSource then sits very nicely in a CollectionContainer along side static elements and everybody plays well together. -- The question I was after here is how to dynamically bind a single element from the viewmodel directly into the CompositeCollection. You definitely get my signature for making CompositeCollection freezable, though. – MojoFilter Dec 06 '09 at 04:09
  • @MojoFilter: How can a `CollectionContainer` contain a `CollectionViewSource`? Doesn't seem to be possible via the `Collection` property. – O. R. Mapper Oct 31 '13 at 14:20
  • @O.R.Mapper: That's exactly how you do it. In most cases, you would use a binding to the `CollectionViewSource` as a static resource for the value of the `Collection` property. – MojoFilter Oct 31 '13 at 18:19
1

Can't you just expose a new collection from your ViewModel that the tree can bind to?

Something like:

public Server LocalServer;
public ObservableCollection<Server> RemoteServers;

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

After all your ViewModel is a ViewModel. It should be exposing exactly what is needed by the view.

Grokys
  • 16,228
  • 14
  • 69
  • 101
  • I could, but the view needs to inject this user-interface idea of an item to click within the tree itself to add new remote nodes. That's very much a view concept instead of a viewmodel concept and in order to keep unit testing viable and simple, it really needs to be separated that way. Wrapping the local server in a collection really kind of solves itself, but I would really like to see an idea of how to bind to that single element. – MojoFilter Dec 10 '09 at 14:29
1

Finally, just after a few years, my WPF skills are good enough to solve this one ;)

Here's a SingleElement like you outlined in your question. It is implemented by subclassing a CollectionContainer and putting the bound element inside the collection. By registering a change handler we can even update the CollectionContainer when the binding changes. For the original CollectionProperty we specify a coercion handler to prevent users of our class to mess with the collection property, if you would like to improve the protection you could use a custom collection instead of an ObservableCollection. As a bonus I show how to make the SingleElement disappear by using a placeholder value, though technically that would be more of an "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); }
    }
}

You can use it exactly like you stated it in your question, except that you have to adjust for the lack of a DataContext in the CompositeCollection:

<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>
Zarat
  • 2,584
  • 22
  • 40
0

Well, this is the closest I can come to your requirements. All the functionality is not contained within one TreeView, nor is it bound to a compositecollection, but that can remain a secret between you and me;)

<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; }
    }
}
Dabblernl
  • 15,831
  • 18
  • 96
  • 148