0

I have two user controls, one contains a TreeView, one contains a ListView.

The TreeView has an itemsource and hierarchical data templates that fill the nodes and leafes (node=TvShow, leaf=Season).

The ListView should show the children of the selected TreeView item (thus, the selected season): the episodes of that season.

This worked fine when I had both the TreeView and the Listview defined in the same window, I could use something like this:

<ListView
    x:Name="_listViewEpisodes"
    Grid.Column="2"
    ItemsSource="{Binding ElementName=_tvShowsTreeView, Path=SelectedItem.Episodes}">

How can I achieve this, when both controls are defined in separate user controls? (because in the context of one user control, I miss the context of the other user control)

This seems something pretty basic and I am getting frustrated that I can't figure it out by myself. I refuse to solve this with code-behind, I have a very clean MVVM project so far and I would like to keep it that way.

Hope that somebody can give me some advise!

bas
  • 13,550
  • 20
  • 69
  • 146
  • You tagged it MVVM, so bind both to a SelectedEpisodes property on your VM. – H H Oct 12 '14 at 10:14
  • The right direction is bind both the controls (treeview and listview) to the same viewmodel. However I wonder how we can bind the `TreeView`'s `SelectedItem` to the viewmodel. As I see the `SelectedItem` of TreeView is ***readonly***. Unless you implement your viewmodel as a `DependencyObject` to support some `SelectedItem` bindable property. But that way we still need some code behind. When using codebehind, we can also handle the `SelectedItemChanged` event of the `TreeView` and update the `SelectedItem` of the viewmodel accordingly. – King King Oct 12 '14 at 10:29
  • @KingKing the best solution in this case is to create a wrapper object with two attached properties binded to each other. Thus we'll bind the first prop to the SelectedItem, and the second one to the viewmodel – Alexis Oct 12 '14 at 10:32
  • @Alexis looks like you are an expert. Not sure. The solution I tried above in fact works OK, although it requires a little code behind due to the fact that `SelectedItem` of TreeView is readonly. If possible you should post an answer to this question. Defining even a wrapper is some kind of using code behind indeed. We in fact cannot avoid code behind, we can just avoid using it as much as possible. – King King Oct 12 '14 at 10:45
  • 1
    @Alexis would you be so kind to add an example of how the wrapper with 2 attached properties would solve my problem? Maybe I am going to far with my 'no-code-behind' policy, but every time I keep going to achieve this I end up with really clean code, so I remain hopeful. Thanks in advance! – bas Oct 12 '14 at 13:47
  • @bas I've created an answer to demonstrate the approach I'm talking about, please check it out =) – Alexis Oct 12 '14 at 17:31
  • @Alexis will do! thanks a lot! kind of busy at work at the moment, but I will not forget this and will upvote + accept when I have the time to give this a try. Thanks again! – bas Oct 13 '14 at 16:30

2 Answers2

2

First of all you have to created the SelectedValue proeprty in your ViewModel and bind the TreeView.SelectedItem property to it. Since the SelectedItem property is read-only I suggest you to create a helper to create OneWayToSource-like binding. The code should be like the following:

    public class BindingWrapper {
    public static object GetSource(DependencyObject obj) { return (object)obj.GetValue(SourceProperty); }
    public static void SetSource(DependencyObject obj, object value) { obj.SetValue(SourceProperty, value); }
    public static object GetTarget(DependencyObject obj) { return (object)obj.GetValue(TargetProperty); }

    public static void SetTarget(DependencyObject obj, object value) { obj.SetValue(TargetProperty, value); }
    public static readonly DependencyProperty TargetProperty = DependencyProperty.RegisterAttached("Target", typeof(object), typeof(BindingWrapper), new PropertyMetadata(null));
    public static readonly DependencyProperty SourceProperty = DependencyProperty.RegisterAttached("Source", typeof(object), typeof(BindingWrapper), new PropertyMetadata(null, OnSourceChanged));

    static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
        SetTarget(d, e.NewValue);
    }
}

The idea is simple: you have two attached properties, the Source and the Target. When the first one changes the PropertyChangedCallback is called and you simply setting the NewValue as the Target property value. In my opinion this scenario is helpful in a lot of cases when you need to bind the read-only property in XAML (especially in control templates).

I've created a simple model to demonstrate how to use this helper:

  public class ViewModel : INotifyPropertyChanged {
    public ViewModel() {
        this.values = new ObservableCollection<string>()
        {
            "first",
            "second",
            "third"
        };
    }
    ObservableCollection<string> values;
    string selectedValue;
    public ObservableCollection<string> Values { get { return values; } }        

    public string SelectedValue {
        get { return selectedValue; }
        set {
            if (Equals(selectedValue, values))
                return;
            selectedValue = value;
            if (PropertyChanged == null)
                return;
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedValue"));
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

So, we have data source, selected value and we'll bind it like this:

    <StackPanel>
    <TreeView ItemsSource="{Binding Values}" 
              local:BindingWrapper.Source="{Binding SelectedItem, RelativeSource={RelativeSource Self}, Mode=OneWay}"
              local:BindingWrapper.Target="{Binding SelectedValue, Mode=OneWayToSource}"
              >
        <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}">
                <Setter Property="Header" Value="{Binding}"/>
            </Style>
        </TreeView.ItemContainerStyle>
    </TreeView>
    <TextBlock Text="{Binding SelectedValue}"/>
</StackPanel>

In the TreeView bound to the ItemsSource from the ViewModel I've created two bindings so they are changing the SelectedValue property in your ViewModel. TextBlock in the end of the sample is used just to show that this approach works.

About the very clean MVVM - I think that it is not the same as the "no code-behind". In my sample the ViewModel still doesn't know anything about your view and if you'll use another control to show your data e.g. ListBox you will be able to use the simple two-way binding and the "BindingWrapper" helper will not make your code unreadable or unportable or anything else.

Alexis
  • 815
  • 6
  • 15
0

Create a SelectedSeason property in your ViewModel and bind the ListView's ItemsSource to SelectedSeason.Episodes.

In a perfect world, you could now use a Two-Way binding in the TreeView to automatically update this property when the SelectedItem changes. However, the TreeView's SelectedItem property is readonly and cannot be bound. You can use just a little bit of code-behind and create an event handler for the SelectionChanged event of the TreeView to update your ViewModel's SelectedSeason there. IMHO this doesn't violate the the MVVM principles.

If you want a pure XAML solution, that a look at this answer.

Community
  • 1
  • 1
Daniel Sklenitzka
  • 2,116
  • 1
  • 17
  • 25