2

Abstract:

I have two UserControls named Zone and ZoneGroup. One of these controls (ZoneGroup) includes two instances of the other one (Zone). Both of them set DataContext of the root element to this, at the Loaded event-handler.

The problem is that DataContext of inner controls (Zones) is set before loading (the DataContextChanged event occurred before Loaded) which causes some malfunctions in UI. (inside Zone controls initial state is wrong.) If I prevent it, everything works fine (at least seems to be!) except I encounter with the following error report. (In the Output window)

public partial class Zone : UserControl
{
    ∙∙∙

    private void Zone_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        // Adding this if-clause solve UI problems but makes some binding errors!
        if (this.IsLoaded)
            brdRoot.DataContext = this;
    }
}

System.Windows.Data Error: 40 : BindingExpression path error: 'ZoneBrush' property not found on 'object' ''ZoneGroup' (Name='')'. BindingExpression:Path=ZoneBrush; DataItem='ZoneGroup' (Name=''); target element is 'brdRoot' (Name=''); target property is 'BorderBrush' (type 'Brush')

Details:

There is a UserControl named Zone containing several data-bindings like so..

<UserControl x:Class="MyApp.Zone"
    ∙∙∙>
    <Border x:Name="brdRoot" BorderBrush="{Binding ZoneBrush}" BorderThickness="1">
        ∙∙∙
    </Border>
</UserControl>

So, I set brdRoot data-context as

public partial class Zone : UserControl
{
    public Brush ZoneBrush
    {
        get { return (Brush)GetValue(ZoneBrushProperty); }
        set { SetValue(ZoneBrushProperty, value); }
    }

    ∙∙∙

    public Zone()
    {
        InitializeComponent();
    }

    private void Zone_Loaded(object sender, RoutedEventArgs e)
    {
        brdRoot.DataContext = this;
    }

    ∙∙∙
}

Also, there is another UserControl that has two ContentPresenters in order to contain and manage two Zone controls.

<UserControl x:Class="MyApp.ZoneGroup"
    ∙∙∙>
    <Border x:Name="brdRoot" BorderBrush="Gray" BorderThickness="1">
        <StackPanel Orientation="Horizontal">
            <ContentPresenter Content="{Binding MainZone}"
                              Margin="{Binding MainZonePadding}"/>
            <ContentPresenter Content="{Binding MemberZone}"/>
        </StackPanel>
    </Border>
</UserControl>

And the code-behind is:

public partial class ZoneGroup : UserControl
{
    public Thickness MainZonePadding
    {
        get { return (Thickness)GetValue(MainZonePaddingProperty); }
        set { SetValue(MainZonePaddingProperty, value); }
    }

    public Zone MainZone
    {
        get { return (Zone)GetValue(MainZoneProperty); }
        set { SetValue(MainZoneProperty, value); }
    }

    public Zone MemberZone
    {
        get { return (Zone)GetValue(MemberZoneProperty); }
        set { SetValue(MemberZoneProperty, value); }
    }


    public ZoneGroup()
    {
        InitializeComponent();
    }

    private void ZoneGroup_Loaded(object sender, RoutedEventArgs e)
    {
        brdRoot.DataContext = this;
    }

    ∙∙∙
}

Edit ► Sketch: Two zone controls in a zone-group control.

My app works fine as expected, but some BindingExpression errors are reported.

Mehdi
  • 2,194
  • 2
  • 25
  • 39
  • This is all plain wrong. That's why it's malfunctioning. [UI is not Data](http://stackoverflow.com/questions/14381402/wpf-programming-methodology/14382137#14382137), therefore your `UserControl`s are not the right place to store data. Create a ViewModel instead and you will not have these issues. – Federico Berasategui Mar 28 '13 at 18:00
  • @HighCore Would you mind please explaining more? What should I do exactly? – Mehdi Mar 28 '13 at 18:03
  • post a screenshot of what you need and what you currently have. – Federico Berasategui Mar 28 '13 at 18:05
  • @HighCore This is my `View`. Zone controls should be put together in a group. Basic controls like `Zone`s have several DPs that are managed by container ones like the `ZoneGroup` control and so on. Finally, main container controls that have some `Model` relevant properties interact with the database using some intermediate class. (my supposed ViewModel). Is this a good plan? – Mehdi Mar 28 '13 at 18:43
  • 2
    sounds ok, except for the fact that the `DataContext` of any given UI element should be its corresponding ViewModel, and not itself. Then if you need to bind to parent properties in the Visual Tree, use `RelativeSource` bindings. – Federico Berasategui Mar 28 '13 at 18:46
  • 3
    also, a container that contains several items is an `ItemsControl`, and not just a custom usercontrol that has some properties. – Federico Berasategui Mar 28 '13 at 18:46
  • Thanks a lot @HighCore :) I'll try to use an `ItemsControl` instead. Also I'll separate ViewModel class from View. Hope to solve this issue. – Mehdi Mar 28 '13 at 18:52
  • @HighCore, You said `DataContext` of any given UI element should be its corresponding `VM`. I read many posts, articles and some parts of several books about MVVM but couldn't find my answer :( How can I have a `UserControl` inside another while both of them have their own `VM`? The container `UserControl` needs some of child `UserControl` DPs to control them. Unfortunately the story hasn't finished yet. There remained some other DPs that should be set directly in the container `UserControl`. what about them? (All `UserControl` DPs are placed in its `VM`.) Please help me! – Mehdi Apr 03 '13 at 14:41

2 Answers2

1

This is not a direct answer!

As @HighCore said, I tried to use an ItemsControl instead of implementing two ContentPresenters in my user-control. Just for clarity I made a new simple app to be able to describe it simply. So please consider some new assumptions:

Here again, there are two UserControls; MyItem and MyItemsControl as follows.

<UserControl x:Class="MyApp.MyItem"
             ∙∙∙>
    <Grid x:Name="grdRoot">
        <Border BorderBrush="{Binding ItemBorderBrsuh}" BorderThickness="1">
            <TextBlock x:Name="txtColorIndicator"
                       Text="Item"
                       TextAlignment="Center"
                       Margin="5"/>
        </Border>
    </Grid>
</UserControl>

C# code-behind:

public partial class MyItem : UserControl
{
    #region ________________________________________  ItemBorderBrsuh

    public Brush ItemBorderBrsuh
    {
        get { return (Brush)GetValue(ItemBorderBrsuhProperty); }
        set { SetValue(ItemBorderBrsuhProperty, value); }
    }

    public static readonly DependencyProperty ItemBorderBrsuhProperty =
        DependencyProperty.Register("ItemBorderBrsuh",
                                    typeof(Brush),
                                    typeof(MyItem),
                                    new FrameworkPropertyMetadata(new SolidColorBrush(Colors.Black), FrameworkPropertyMetadataOptions.None, OnItemBorderBrsuhPropertyChanged));

    private static void OnItemBorderBrsuhPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        MyItem instance = sender as MyItem;

        if (instance != null && e.NewValue is SolidColorBrush)
            instance.txtColorIndicator.Text = (e.NewValue as SolidColorBrush).Color.ToString();
    }

    #endregion

    public MyItem()
    {
        InitializeComponent();

        grdRoot.DataContext = this;
    }
}

And this is the MyItemsControl.

<UserControl x:Class="MyApp.MyItemsControl"
    ∙∙∙>
    <StackPanel>
        <TextBlock x:Name="txtHeader" Margin="0,0,0,5" TextAlignment="Center" Text="0 Item(s)"/>
        <Border BorderBrush="Gray" BorderThickness="1" Padding="5">
            <ItemsControl x:Name="itemsControl" ItemsSource="{Binding Items}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>

                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <local:MyItem />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Border>
    </StackPanel>
</UserControl>

C# Code-behind:

public partial class MyItemsControl : UserControl
{
    private ObservableCollection<MyItem> _Items = new ObservableCollection<MyItem>();
    public ObservableCollection<MyItem> Items
    {
        get
        {
            return _Items;
        }
        set
        {
            _Items = value;
        }
    }

    private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        txtHeader.Text = Items.Count + " Item(s)";
    }

    public MyItemsControl()
    {
        InitializeComponent();
        Items.CollectionChanged += Items_CollectionChanged;

        this.DataContext = this;
    }
}

Here is how to use MyItem within MyItemsControl.

<Grid>
    <local:MyItemsControl HorizontalAlignment="Center" VerticalAlignment="Center" Padding="5" BorderBrush="Black" BorderThickness="1">
        <local:MyItemsControl.Items>
            <local:MyItem ItemBorderBrsuh="Green" Margin="1"/>
            <local:MyItem ItemBorderBrsuh="Red" Margin="1"/>
            <local:MyItem ItemBorderBrsuh="Blue" Margin="1"/>
            <local:MyItem ItemBorderBrsuh="Orange" Margin="1"/>
        </local:MyItemsControl.Items>
    </local:MyItemsControl>
</Grid>

Now, there is no problem with BindingExpressions, but an important question remains. How to replace

{
    grdRoot.DataContext = this;
}

and

{
    this.DataContext = this;
}

with a true ViewModel?

Screenshot:

MyItemsControl


Edit: I tried to implement MVVM pattern but there are some problems. I asked the first one here.

Community
  • 1
  • 1
Mehdi
  • 2,194
  • 2
  • 25
  • 39
1

You're overcomplicating it all too much, with all those UserControls and all those DependencyProperties. Look at this sample which uses 0 lines of C# code (XAML-Only):

<Window x:Class="MiscSamples.ItemsControl"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ItemsControl" Height="300" Width="300">
    <Window.Resources>

Style for the ItemsControl:

        <Style TargetType="ItemsControl" x:Key="ZoneItemsControlStyle">
            <Setter Property="BorderBrush" Value="Black"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="HorizontalContentAlignment" Value="Center"/>
            <Setter Property="Foreground" Value="Black"/>
            <Setter Property="Padding" Value="5"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ItemsControl">
                        <Border BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Background="{TemplateBinding Background}"
                                Padding="5">
                            <DockPanel>
                                <TextBlock HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                           Text="{Binding Items.Count,RelativeSource={RelativeSource TemplatedParent}, StringFormat='{}{0} Item(s)'}"
                                           Foreground="{TemplateBinding Foreground}"
                                           DockPanel.Dock="Top"/>
                                <Border BorderBrush="{TemplateBinding BorderBrush}"
                                        BorderThickness="{TemplateBinding BorderThickness}"
                                        Padding="{TemplateBinding Padding}">
                                    <ItemsPresenter/>
                                </Border>
                            </DockPanel>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal" IsItemsHost="True"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>

DataTemplate For Brushes:

        <DataTemplate DataType="{x:Type Brush}">
            <Border BorderBrush="{Binding}" Margin="1" BorderThickness="1" Padding="2,3,2,3">
                <TextBlock Text="{Binding}" TextAlignment="Center" Foreground="Black"/>
            </Border>
        </DataTemplate>

    </Window.Resources>

Now, its usage:

    <Grid>
        <ItemsControl VerticalAlignment="Center" HorizontalAlignment="Center"
                      Style="{StaticResource ZoneItemsControlStyle}">
            <SolidColorBrush Color="Red"/>
            <SolidColorBrush Color="Green"/>
            <SolidColorBrush Color="Black"/>
            <SolidColorBrush Color="Blue"/>
        </ItemsControl>
    </Grid>
</Window>

Result:

enter image description here

See how I'm making use of DataTemplates in order to show a custom piece of UI for a specific Data Type? (In this case, System.Windows.Media.Brush class)

I'm "using the Brushes as ViewModels". You could also create your own ViewModels of course, and then create a specific DataTemplate for each VM type.

Also, see how I'm using the TemplateBinding MarkupExtension to bind several properties inside the ControlTemplate to the corresponding value in the ItemsControl instance.

Finally, see how you can actually add ANY kind of items to the ItemsControl.

Also, I must mention that I used this Style-based approach in order to enable reusability. You could place another ItemsControl somewhere else in the application and set its Style="{StaticResource ZoneItemsControlStyle}" and you're done. But if you only plan to use this a single time, you can just place all the properties hardcoded in the ItemsControl.Template ControlTemplate.

Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154