13

I have been unable to find a clean, simple, example of how to correctly implement a usercontrol with WPF that has a DependencyProperty within the MVVM framework. My code below fails whenever I assign the usercontrol a DataContext.

I am trying to:

  1. Set the DependencyProperty from the calling ItemsControl , and
  2. Make the value of that DependencyProperty available to the ViewModel of the called usercontrol.

I still have a lot to learn and sincerely appreciate any help.

This is the ItemsControl in the topmost usercontrol that is making the call to the InkStringView usercontrol with the DependencyProperty TextInControl (example from another question).

<ItemsControl ItemsSource="{Binding Strings}" x:Name="self" >

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Orientation="Vertical" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>


    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <DataTemplate.Resources>
                <Style TargetType="v:InkStringView">
                    <Setter Property="FontSize" Value="25"/>
                    <Setter Property="HorizontalAlignment" Value="Left"/>
                </Style>
            </DataTemplate.Resources>

            <v:InkStringView TextInControl="{Binding text, ElementName=self}"  />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Here is the InkStringView usercontrol with the DependencyProperty.

XAML:

<UserControl x:Class="Nova5.UI.Views.Ink.InkStringView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         x:Name="mainInkStringView"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>   
            <RowDefinition/>
        </Grid.RowDefinitions>

        <TextBlock Grid.Row="0" Text="{Binding TextInControl, ElementName=mainInkStringView}" />
        <TextBlock Grid.Row="1" Text="I am row 1" />
    </Grid>
</UserControl>

Code-Behind file:

namespace Nova5.UI.Views.Ink
{
    public partial class InkStringView : UserControl
    {
        public InkStringView()
        {
            InitializeComponent();
            this.DataContext = new InkStringViewModel();   <--THIS PREVENTS CORRECT BINDING, WHAT
        }                                                   --ELSE TO DO?????

        public String TextInControl
        {
            get { return (String)GetValue(TextInControlProperty); }
            set { SetValue(TextInControlProperty, value); }
        }

        public static readonly DependencyProperty TextInControlProperty =
            DependencyProperty.Register("TextInControl", typeof(String), typeof(InkStringView));

    }
}
Alan Wayne
  • 5,122
  • 10
  • 52
  • 95
  • The DataContext of the controls in the ItemTemplate of an ItemsControl (or any derived class) is automatically assigned by WPF to the respective item in the source collection. In your case this would be an element from the `String` collection. If `Strings` is a collection of objects that have a `text` property, you would simply write the binding in the DataTemplate as `TextInControl="{Binding text}"` and *not explicitly set* any other DataContext. I'd suggest to read up on this topic. – Clemens Sep 04 '14 at 18:33
  • See [Data Binding Overview](http://msdn.microsoft.com/en-us/library/ms752347.aspx) and [Data Templating Overview](http://msdn.microsoft.com/en-us/library/ms742521.aspx). – Clemens Sep 04 '14 at 18:33
  • @Clemens Hi. Dropping the ElementName= ... from the calling ItemsControl still ends up with "I am row 1" being displayed and a blank line where textblock on row 0 should be. ??? Ideas? – Alan Wayne Sep 04 '14 at 18:45
  • Is Strings an ObservableCollection? – Lee O. Sep 04 '14 at 18:46
  • @Lee O Yes. Strings is an ObservableCollection in the ViewModel for the intial calling ItemsControl. As an aside, if I replace the usercontrol with a simple TextBlock, all appears and runs correctly with the above bindings. – Alan Wayne Sep 04 '14 at 18:51
  • It is an ObservableCollection *of what type*? `string` or some class that has a `text` property of type string? – Clemens Sep 04 '14 at 18:53
  • @Clemens Changing the UserControl to use RelativeSource still resulted in a blank line being printed. The ObservableCollection is of a class with public string text { get { return _text; } set { if (_text != value) { _text = value; OnPropertyChanged("text"); } } } – Alan Wayne Sep 04 '14 at 19:07
  • You have removed the DataContext assignment from the UserControl's constructor? – Clemens Sep 04 '14 at 19:09
  • All this for nothing. Seriously, designing a user control with its own viewmodel is a well-known code smell. You're experiencing these issues because of it. Get rid of the view model and you're done. –  Sep 04 '14 at 19:51

4 Answers4

16

That is one of the many reasons you should never set the DataContext directly from the UserControl itself.

When you do so, you can no longer use any other DataContext with it because the UserControl's DataContext is hardcoded to an instance that only the UserControl has access to, which kind of defeats one of WPF's biggest advantages of having separate UI and data layers.

There are two main ways of using UserControls in WPF

  1. A standalone UserControl that can be used anywhere without a specific DataContext being required.

    This type of UserControl normally exposes DependencyProperties for any values it needs, and would be used like this:

    <v:InkStringView TextInControl="{Binding SomeValue}" />
    

    Typical examples I can think of would be anything generic such as a Calendar control or Popup control.

  2. A UserControl that is meant to be used with a specific Model or ViewModel only.

    These UserControls are far more common for me, and is probably what you are looking for in your case. An example of how I would use such a UserControl would be this:

    <v:InkStringView DataContext="{Binding MyInkStringViewModelProperty}" />
    

    Or more frequently, it would be used with an implicit DataTemplate. An implicit DataTemplate is a DataTemplate with a DataType and no Key, and WPF will automatically use this template anytime it wants to render an object of the specified type.

    <Window.Resources>
        <DataTemplate DataType="{x:Type m:InkStringViewModel}">
            <v:InkStringView />
        </DataTemplate>
    <Window.Resources>
    
    <!-- Binding to a single ViewModel -->
    <ContentPresenter Content="{Binding MyInkStringViewModelProperty}" />
    
    <!-- Binding to a collection of ViewModels -->
    <ItemsControl ItemsSource="{Binding MyCollectionOfInkStringViewModels}" />
    

    No ContentPresenter.ItemTemplate or ItemsControl.ItemTemplate is needed when using this method.

Don't mix these two methods up, it doesn't go well :)


But anyways, to explain your specific problem in a bit more detail

When you create your UserControl like this

<v:InkStringView TextInControl="{Binding text}"  />

you are basically saying

var vw = new InkStringView()
vw.TextInControl = vw.DataContext.text;

vw.DataContext is not specified anywhere in the XAML, so it gets inherited from the parent item, which results in

vw.DataContext = Strings[x];

so your binding that sets TextInControl = vw.DataContext.text is valid and resolves just fine at runtime.

However when you run this in your UserControl constructor

this.DataContext = new InkStringViewModel();

the DataContext is set to a value, so no longer gets automatically inherited from the parent.

So now the code that gets run looks like this:

var vw = new InkStringView()
vw.DataContext = new InkStringViewModel();
vw.TextInControl = vw.DataContext.text;

and naturally, InkStringViewModel does not have a property called text, so the binding fails at runtime.

Rachel
  • 130,264
  • 66
  • 304
  • 490
  • 1
    any good resource to learn on the two methods you explained above? – Ehsan Sajjad Sep 04 '14 at 20:00
  • 1
    @EhsanSajjad I can't think of any right now, but they're pretty easy concepts to remember once you realize how it works. Either **A)** write your UserControl XAML in such a way that it won't break regardless of what the `DataContext` is (usually done by using DependencyProperties, like OP has), or **B)** write your UserControl XAML with the assumption that the `DataContext` is of a specific data type (typically a Model or ViewModel). And at no time should you ever set the `DataContext` property from within the UserControl itself :) – Rachel Sep 04 '14 at 20:10
  • @Rachel So...In case 2, the ViewModel of the UserControl is actually the same ViewModel of the calling module?? I'm not familiar with ContentPresenter...where does it fit in? – Alan Wayne Sep 04 '14 at 20:12
  • @AlanWayne Yes, the ViewModel in case #2 would be the one from the `DataContext` of whatever created the UserControl. In the OPs case, it could be whatever `Strings[x]` is. – Rachel Sep 04 '14 at 20:17
  • @Alan And the `ContentPresenter` is an object used to display any object in the UI, even a non-UI object such as a ViewModel. It's actually used by the `ItemsControl` to render whatever content is inside the `ItemsSource` collection (For an example of how an `ItemsControl` renders, see the Snoop screenshot at the top of [my blog post about ItemsControl](http://rachel53461.wordpress.com/2011/09/17/wpf-itemscontrol-example/)). I thought it would be simpler to just write the `` tag than to write the `` tag and explain how it renders objects in the UI :) – Rachel Sep 04 '14 at 20:19
  • @Rachel Can an implicit DataTemplate be used with a parent DataType? That is can the DataType be one that is inherited by multiple ViewModels so the usercontrol can be used by multiple viewmodels? I am asking because in this case I was trying to add voice recognition via the usercontrol as imput to my business class. But I can easily see where handwriting, keyboard, or motion could be used. I am hoping to place the input concerns into different usercontrols and allow each business viewmodel to have use of each of the different usercontrols. How can this be done? – Alan Wayne Sep 05 '14 at 02:12
  • @AlanWayne I'm not sure if I'm entirely clear on your question - Do you want one View for multiple Models, or multiple Models for one view? If the first, it would be easy to test out and worst case scenario is you have separate `DataTemplates` defined for each of your child `ViewModel` types, which is no big deal. If the second, it depends on what kind of event triggers the change. It might be best to ask a new question with all the details if that's the case :) – Rachel Sep 05 '14 at 04:55
5

You're almost there. The problem is that you're creating a ViewModel for your UserControl. This is a smell.

UserControls should look and behave just like any other control, as viewed from the outside. You correctly have exposed properties on the control, and are binding inner controls to these properties. That's all correct.

Where you fail is trying to create a ViewModel for everything. So ditch that stupid InkStringViewModel and let whoever is using the control to bind their view model to it.

If you are tempted to ask "what about the logic in the view model? If I get rid of it I'll have to put code in the codebehind!" I answer, "is it business logic? That shouldn't be embedded in your UserControl anyhow. And MVVM != no codebehind. Use codebehind for your UI logic. It's where it belongs."

  • Logically every component has a model, sometimes it is split in n properties (FirstName, LastName, Address...), sometimes you have a single composite object (Person), sometimes a whole model with heterogeneous data. – Pragmateek Sep 04 '14 at 19:44
  • 2
    @Pragmateek and sometimes the cow jumps over the moon, but that also doesn't have a damn thing to do with the question or this answer. –  Sep 04 '14 at 19:50
  • I was adding this comment because this is the first remark of your answer ("this is a smell") which is not always true, just wanted to make it clear. And please don't be fooled by the coincidence I'm not the downvoter (I never downvote as my profile would show you and especially not somebody with as much reputation as you have) :) – Pragmateek Sep 04 '14 at 20:03
  • @Will Thanks. You are mostly right, in that I am wondering how to manage the code logic associated with the usercontrol if I don't have either code-behind or a viewmodel. However, in this case the logic is not business logic but rather implementation of additional functions that are not logically part of the business code. – Alan Wayne Sep 04 '14 at 23:24
  • 2
    @Pragmateek Eh, I've been around long enough to not worry about downvotes :) I would, however, consider it always a smell when you find yourself creating a viewmodel specifically for use by a usercontrol. It just doesn't ever work correctly. –  Sep 05 '14 at 15:20
  • 2
    @AlanWayne If it's UI code, throw it in the codebehind, as I said. You need to refactor business logic out of your user control, and that doesn't mean create a new VM. Perhaps your user control contains too much of your UI? Maybe it should be broken down so that your business logic would fit nicely into the VM for that page/window in which the user control(s) are used? Would be hard to tell you exactly how it could be done if I had your code, but a reasonable guide is to say "What does my UI need? Add a property so VM binds can give it to me." –  Sep 05 '14 at 15:23
3

Seems like you are mixing the model of the parent view with the model of the UC.

Here is a sample that matches your code:

The MainViewModel:

using System.Collections.Generic;

namespace UCItemsControl
{
    public class MyString
    {
        public string text { get; set; }
    }

    public class MainViewModel
    {
        public ObservableCollection<MyString> Strings { get; set; }

        public MainViewModel()
        {
            Strings = new ObservableCollection<MyString>
            {
                new MyString{ text = "First" },
                new MyString{ text = "Second" },
                new MyString{ text = "Third" }
            };
        }
    }
}

The MainWindow that uses it:

<Window x:Class="UCItemsControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:v="clr-namespace:UCItemsControl"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <v:MainViewModel></v:MainViewModel>
    </Window.DataContext>
    <Grid>
        <ItemsControl 
                ItemsSource="{Binding Strings}" x:Name="self" >

            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Orientation="Vertical" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>


            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <DataTemplate.Resources>
                        <Style TargetType="v:InkStringView">
                            <Setter Property="FontSize" Value="25"/>
                            <Setter Property="HorizontalAlignment" Value="Left"/>
                        </Style>
                    </DataTemplate.Resources>

                    <v:InkStringView TextInControl="{Binding text}"  />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

Your UC (no set of DataContext):

public partial class InkStringView : UserControl
{
    public InkStringView()
    {
        InitializeComponent();
    }

    public String TextInControl
    {
        get { return (String)GetValue(TextInControlProperty); }
        set { SetValue(TextInControlProperty, value); }
    }

    public static readonly DependencyProperty TextInControlProperty =
        DependencyProperty.Register("TextInControl", typeof(String), typeof(InkStringView));
}

(Your XAML is OK)

With that I can obtain what I guess is the expected result, a list of values:

First
I am row 1
Second
I am row 1
Third
I am row 1
Pragmateek
  • 13,174
  • 9
  • 74
  • 108
2

You need to do 2 things here (I'm assuming Strings is an ObservableCollection<string>).

1) Remove this.DataContext = new InkStringViewModel(); from the InkStringView constructor. The DataContext will be one element of the Strings ObservableCollection.

2) Change

<v:InkStringView TextInControl="{Binding text, ElementName=self}"  />

to

<v:InkStringView TextInControl="{Binding }" />

The xaml you have is looking for a "Text" property on the ItemsControl to bind the value TextInControl to. The xaml I put using the DataContext (which happens to be a string) to bind TextInControl to. If Strings is actually an ObservableCollection with a string Property of SomeProperty that you want to bind to then change it to this instead.

<v:InkStringView TextInControl="{Binding SomeProperty}" />
Lee O.
  • 3,212
  • 2
  • 26
  • 36
  • The assumption is wrong. `Strings` is a collection of objects with a public `text` property of type string. – Clemens Sep 04 '14 at 19:11
  • yep...so use the bottom option – Lee O. Sep 04 '14 at 19:12
  • I tested it both ways using his code and this fixes it as you assumed in your comments. The binding for TextInControl is fine as is though. – Lee O. Sep 04 '14 at 19:13
  • @Lee O Removing the DataContext = ... from the code-behind does correctly display the "SomeProperty" when set to text. However, I no longer have a datacontext for the usercontrol. – Alan Wayne Sep 04 '14 at 20:21
  • @LeeO. #2 will change it from binding to `ItemsControl.text` to `String[x].text`, however I still don't think that will work if the `DataContext` is still being set in the UserControl constructor, as then it's trying to bind to `InkStringViewModel.text` which doesn't sound like it exists. It might work with ``, as then it finds the `` item that wraps the item and binds to `DataContext.text`, which should be `Strings[x].text` but I'm not sure – Rachel Sep 04 '14 at 21:07
  • @Rachel Sorry if I wasn't clear but I was implying that they needed to do both 1 & 2 to make it work. – Lee O. Sep 04 '14 at 21:09