2

This is my first time posting a question. I've simplified my code as much as possible to illustrate what I'm looking for.

I have a ViewModel (outer) that contains an ObservableCollection of another ViewModel (inner). The inner ViewModel is for a UserControl. The outer ViewModel is for MainWindow. I simply want to display one UserControl for each item in the ObservableCollection. But, I'm having trouble getting the UserControl's DataContext set to the items in the ObservableCollection.

Inner ViewModel (for UserControl):

public class InnerViewModel : ViewModelBase
{
    string _text;

    public string Text
    {
        get { return _text; }
        set { SetProperty<string>(ref _text, value); }
    }

    public InnerViewModel() { }
}

Inner ViewModel (for UserControl):

public class OuterViewModel : ViewModelBase
{
    ObservableCollection<InnerViewModel> _innerViewModels;

    public ObservableCollection<InnerViewModel> InnerViewModels
    {
        get { return _innerViewModels; }
        set { SetProperty<ObservableCollection<InnerViewModel>>(ref _innerViewModels, value); }
    }

    public OuterViewModel()
    {
        _innerViewModels = new ObservableCollection<InnerViewModel>();
    }

    public void Init()
    {
        InnerViewModels.Clear();
        InnerViewModels.Add(new InnerViewModel { Text = "Item1" });
        InnerViewModels.Add(new InnerViewModel { Text = "Item2" });
    }
}

InnerControl XAML (outermost tag removed for cleanliness)

<UserControl.DataContext>
    <local:InnerViewModel />
</UserControl.DataContext>
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="50px"></ColumnDefinition>
        <ColumnDefinition ></ColumnDefinition>
        <ColumnDefinition Width="50px"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Label Content="Header"></Label>
    <Label Grid.Column="1" Content="{Binding Text}" ></Label>
    <Label Grid.Column="2" Content="Footer"></Label>
</Grid>

MainWindow XAML

<Window.DataContext>
    <local:OuterViewModel />
</Window.DataContext>
<Grid>
    <ItemsControl ItemsSource="{Binding InnerViewModels}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <local:InnerControl></local:InnerControl> <!-- HOW DO I SET THE DATACONTEXT ON THIS??? -->
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>

InnerControl.cs Code:

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

MainWindow.cs Code:

public partial class MainWindow : Window
{
    OuterViewModel _vm;

    public MainWindow()
    {
        InitializeComponent();

        _vm = (OuterViewModel)DataContext;
        _vm.Init();
    }
}

ViewModelBase:

public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
    {
        if (Equals(storage, value))
        {
            return false;
        }

        storage = value;
        this.OnPropertyChanged(propertyName);
        return true;
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler eventHandler = this.PropertyChanged;
        if (eventHandler != null)
        {
            eventHandler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Result: Screenshot of what I get when I run

John Hancock
  • 51
  • 1
  • 6
  • im thinking you are using Paul Sheriff form channel9 as a code source? Your viewModelbase has a INOtifyProperty. It seems you arent using the RaisePropertyChage("property") though. – JenoS Jun 10 '16 at 16:20
  • added code for ViewModelBase – John Hancock Jun 10 '16 at 16:27

2 Answers2

3

I solved this as follows:

Changed MainWindow.cs to create the outer view model:

public partial class MainWindow : Window
{
    OuterViewModel _vm;

    public MainWindow()
    {
        InitializeComponent();

        _vm = new OuterViewModel();
        _vm.Init();
        DataContext = _vm;
    }
}

Change MainWindow to NOT have DataContext set

<!-- Don't set DataContext here -->
<Grid>
    <ItemsControl ItemsSource="{Binding InnerViewModels}">
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="{x:Type local:InnerViewModel}">
                <local:InnerControl DataContext="{Binding}"></local:InnerControl>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>

Changed InnerControl XAML to NOT have DataContext set:

<!-- Don't set DataContext here -->
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="50px"></ColumnDefinition>
        <ColumnDefinition ></ColumnDefinition>
        <ColumnDefinition Width="50px"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Label Content="Header"></Label>
    <Label Grid.Column="1" Content="{Binding Text}" ></Label>
    <Label Grid.Column="2" Content="Footer"></Label>
</Grid>
John Hancock
  • 51
  • 1
  • 6
0

In you view for the inner VM you create the the view-model in the view (view-first), that means your view that you create in the DataTemplate has a different view-model than the one supplied by the ItemsControl.

You could maybe overwrite that again like this (not sure about the property assignment order):

<DataTemplate>
    <local:InnerControl DataContext="{Binding}"/>
</DataTemplate>

As noted in the comment, i would not create the VMs in the view, but create the views implicitly using typed DataTemplates.

Community
  • 1
  • 1
H.B.
  • 166,899
  • 29
  • 327
  • 400
  • Added that DataContext bit and got the same result. I'll look into how to "create the views implicitly using typed DataTemplates." Thanks – John Hancock Jun 10 '16 at 16:30
  • You could go a level higher where it's guaranteed to still have the `ItemsControl` item `DataContext`: `{Binding DataContext, RelativeSource={RelativeSource AncestorType=ContentPresenter}}`, of course if your view changes the property after that, this won't work either. It's not a proper solution anyway, more like a dirty hack. – H.B. Jun 10 '16 at 16:32