1

If I have this:

<Grid xmlns:local="clr-namespace:xaml_collections">
    <StackPanel>
        <StackPanel.DataContext>
            <local:AComposite>
                <local:AComposite.TheChildren>
                    <Rectangle
                        Height="85"
                        Width="85"
                        Fill="Red"
                        x:Name="foobar"
                        />
                </local:AComposite.TheChildren>
            </local:AComposite>
        </StackPanel.DataContext>

        <TextBlock DataContext="{Binding TheChildren[0]}">
            <Run Text="{Binding Height}"></Run>
        </TextBlock>

        <TextBlock DataContext="{Binding ChildrenByName[foobar]}">
            <Run Text="{Binding Height}"></Run>
        </TextBlock>
        </StackPanel>
    </Grid>

and this:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Shapes;
using System.Collections.Specialized;

namespace xaml_collections
{
public class AComposite : FrameworkElement
{
    public AComposite()
    {
        if (_TheChildren != null && _TheChildren is ObservableCollection<Rectangle>)
        {
            ((ObservableCollection<Rectangle>)_TheChildren)
                .CollectionChanged += AComposite_CollectionChanged;
        }
    }

    void AComposite_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e != null && e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
        {
            foreach (var anItem in e.NewItems)
            {
                if (anItem is FrameworkElement)
                {
                    FrameworkElement theFrameworkElementItem = (FrameworkElement)anItem;
                    _ChildrenByName.Add(theFrameworkElementItem.Name, theFrameworkElementItem);
                }
            }
        }
    }                   

    private Dictionary<String,FrameworkElement> _ChildrenByName = new Dictionary<String,FrameworkElement>();
    public Dictionary<String,FrameworkElement> ChildrenByName
    {
        get
        {
            return _ChildrenByName;
        }
        private set { }
    }

    private IList _TheChildren = new ObservableCollection<Rectangle>();
    public IList TheChildren
    {
        get
        {  
            return _TheChildren;
        }
        private set { }
    }
}

}

I can see the value of Height which is 85 in the TextBlock for the TextBlock bound to Children[0] at design time. But I can only see the value of ChildrenByName[foobar]. Height at run time. Is there any way to keep these collections synchronized during design time as well?

EDIT

This seems to work and thanks to Nick Miller. The lesson here is suppose is don't try to create derivative collections. Use properties that don't copy the collection, but just refer to it.

public Dictionary<String,FrameworkElement> ByName
{
    get
        {
        return
        this.Children.AsQueryable().Cast<FrameworkElement>()
            .ToDictionary( element => element.Tag.ToString() );
        }
 }

And things like this:

public List<FrameworkElement> Top3
{
    get
    {
        return
        this.Children.AsQueryable().Cast<FramworkElement>().
        OrderByDescending( element => element.Height )
        .Take(3).ToList();
    }
}
Andyz Smith
  • 698
  • 5
  • 20
  • 1
    If you add a design time view model w/ dummy data, it will allow you to see how the UI looks in a "real-world" situation – d.moncada Jan 06 '16 at 18:10
  • @d.moncada Why do I need that? I can already see the 'dummy' data that is initialized in TheChildren, at least in one case. I don't want more 'dummy' stuff floating around if it is not necessary. – Andyz Smith Jan 06 '16 at 18:13
  • May I ask why you are deriving from `FrameworkElement`? – Nicholas Miller Jan 06 '16 at 19:29
  • @NickMiller Good question. I'm not totally sure, but basically I want to be able to use XAML to create these kinds of data contexts, and it seemed natural instead of, what might be....more appropriate -- DependencyObject? I also have some future goal of using the same class hierarchy for both DataContexts *AND* visual representations if that makes any sense. – Andyz Smith Jan 06 '16 at 19:52
  • The reason I asked is that the `DataContext` is usually part of the model/viewmodel and not the view of your application. What exactly is your `AComposite` class supposed to be? Is it supposed to be some sort of container for visual elements? – Nicholas Miller Jan 06 '16 at 22:11
  • @NickMiller I want to put Rectangles into the Composite and then create a Listbox that visually has the Rectangles and their Fill and Stroke, but also I want to be able to use the Composite as the Context for a block of text, with the Area of each rectangle, perhaps each rectangle, identified by Name, or maybe just Rectangles[0] or even Children.LargestArea would give back the largest rectangle as a property that could be bound to in the textblock. You tell me, which part of this are the View and which the ViewModel. – Andyz Smith Jan 07 '16 at 14:21
  • @NickMiller I want to create the children in XAML, as per the wonderfulness of XAML, but then be able to keep derivative collections like dictionary in sync, at design time, no less. – Andyz Smith Jan 07 '16 at 14:24

1 Answers1

1

First, I do not recommend creating multiple collections with the same data. Instead of trying to synchronize these collections, you should instead focus on taking advantage of the data-binding engine built into WPF.

Per your comments it sounds like you want something like the following:

List of Rectangles

This uses your Composite class, but tweaked to use DependencyProperties. These special types of properties are really useful for working in design-time and also allow your properties to partake in the data-binding system. If you haven't already, see the Dependency Properties Overview

Composite.cs

using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Shapes;

namespace _34639801
{
    [ContentProperty("Children")]
    public class Composite : FrameworkElement
    {
        public static readonly DependencyProperty ChildrenProperty = DependencyProperty.Register("Children",
            typeof(IList),
            typeof(Composite),
            new PropertyMetadata(default(IList)));

        [Category("Common")]
        public IList Children
        {
            get { return (IList)GetValue(ChildrenProperty); }
            set { SetValue(ChildrenProperty, value); }
        }

        //Get children by name.
        public Shape this[string name]
        {
            get
            {
                foreach (Shape s in Children)
                {
                    if (s.Name.Equals(name))
                    {
                        return s;
                    }
                }
                return null;
            }
        }

        public Composite()
        {
            SetCurrentValue(ChildrenProperty, new ObservableCollection<Shape>());
        }
    }
}

MainWindow.xaml

<Window x:Class="_34639801.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_34639801"
        mc:Ignorable="d"
        Title="MainWindow" Height="456.061" Width="575.91">
    <Grid>
        <Grid.DataContext>
            <local:Composite>
                <local:Composite.Children>
                    <Rectangle Width="40" Height="20" Fill="Red" Stroke="Black" Name="R1"/>
                    <Rectangle Width="50" Height="20" Fill="Cyan" Stroke="Blue" Name="R2"/>
                    <Rectangle Width="40" Height="20" Fill="Green" Stroke="Black" Name="R3"/>
                    <Rectangle Width="90" Height="10" Fill="Blue" Stroke="Black" Name="R4"/>
                    <Rectangle Width="40" Height="80" Fill="Yellow" Stroke="Black" Name="R5"/>
                    <Rectangle Width="40" Height="10" Fill="Magenta" Stroke="Black" Name="R6"/>
                    <Rectangle Width="30" Height="30" Fill="Orange" Stroke="Black" Name="Square"/>
                </local:Composite.Children>
            </local:Composite>
        </Grid.DataContext>

        <ListBox ItemsSource="{Binding Children}" Margin="10,10,0,10" HorizontalContentAlignment="Stretch" HorizontalAlignment="Left" Width="337">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="Black" BorderThickness="1,1,1,1">
                        <Grid>
                            <TextBlock Margin="6,0,0,0" Text="{Binding Name, StringFormat=Name: {0}}"/>
                            <TextBlock Margin="6,14,0,0" Text="{Binding Width, StringFormat=Width: {0}}"/>
                            <TextBlock Margin="6,28,0,0" Text="{Binding Height, StringFormat=Height: {0}}"/>
                            <TextBlock Margin="6,42,0,0" Text="{Binding Fill, StringFormat=Fill: {0}}"/>
                            <TextBlock Margin="6,56,0,0" Text="{Binding Stroke, StringFormat=Stroke: {0}}"/>
                            <Border BorderBrush="Black" BorderThickness="1,0,0,0" Margin="110,0,0,0" HorizontalAlignment="Stretch" Padding="10">
                                <ContentPresenter Content="{Binding}"/>
                            </Border>
                        </Grid>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <TextBox Margin="352,10,10,377">
            <TextBox.Text>
                <MultiBinding StringFormat="{}Name: {0}, Dimensions: {1} x {2}">
                    <Binding Path="Children[6].Name"/>
                    <Binding Path="Children[6].Width"/>
                    <Binding Path="Children[6].Height"/>
                </MultiBinding>
            </TextBox.Text>
        </TextBox>

        <TextBox Margin="352,54,10,333">
            <TextBox.Text>
                <MultiBinding StringFormat="{}Name: {0}, Dimensions: {1} x {2}">
                    <Binding Path="[Square].Name"/>
                    <Binding Path="[Square].Width"/>
                    <Binding Path="[Square].Height"/>
                </MultiBinding>
            </TextBox.Text>
        </TextBox>
    </Grid>
</Window>

The method of using the 'synchronized' collection was to instead use an indexer found in Composite.cs. I've tried several ways to synchronize the collections in a similar manner to how you set it up, but all had issues and increased in complexity. I believe the reason why it doesn't synchronize in your case for the designer is because you are not signaling an INotifyPropertyChanged.PropertyChanged event.

I encourage you to find alternative solutions to synchronizing multiple collections manually, especially when they have the same data. It is much better to let the WPF binding engine to take care of things for you.

Nicholas Miller
  • 4,205
  • 2
  • 39
  • 62
  • Very nice. Thorough and well-informed. One last caveat though; is it possible to implement a property of return type Shape that upon get access will sort the children and return the one with the greatest Height? – Andyz Smith Jan 07 '16 at 20:19
  • Yes, you could call that property `ChildWithGreatestHeight`, then use the `get` accessor to obtain the desired child. I wouldn't recommend doing a sort there because its rather unconventional. Instead just do a search, this is where `LINQ` comes handy. – Nicholas Miller Jan 07 '16 at 20:29
  • So you think the property will remain synchronized with the overall Children collection? -- RE: Can this all be done without dependency properties, e.g. perhaps properly implementing NotifyPropertyChanged for conventional CLR properties like Shape ChildWithGreatestHeight() return Children.Orderby(Shape => Shape.Height).Last() – Andyz Smith Jan 07 '16 at 20:35
  • The reason why it stays synchronized is because there is no synchronization to be done. The properties are simply accessing the one and only `Children` collection. I'm a bit unclear as to whether or not `DependencyProperties` are necessary in this case, but they certainly do keep the designer informed. According to [this answer](http://stackoverflow.com/a/5472247/975724) it appears you can use `INotifyPropertyChanged` as an alternative, but I didn't test it. – Nicholas Miller Jan 07 '16 at 21:26
  • Not a problem, I'm glad to help. Remember, if you found that this answer solved your problem, please accept it. – Nicholas Miller Jan 08 '16 at 14:58