What is the proper way to designate the data context and bind hierarchical data in the treeview?

883 Views Asked by At

I have a fairly simple data model:

public class ServiceModel
{
    private static readonly IQmiServiceManager ServiceManager = Bootstrap.Instance.DomainManager.QmiServiceManager;

    private string _serviceName;
    private ObservableCollection<string> _serviceMessages;


    public string Name
    {
        get { return _serviceName; }
        private set { _serviceName = value.ToUpper(); }
    }

    public ObservableCollection<string> Messages
    {
        get { return _serviceMessages; }
        private set { _serviceMessages = value; }
    }


    public ServiceModel(string ServiceName, IList<string> ServiceMessages)
    {
        Name = ServiceName;
        Messages = new ObservableCollection<string>(ServiceMessages);
    }

}

...which is encapsulated in this view model:

public class ServiceCollectionViewModel
{
    private readonly ObservableCollection<ServiceModel> _serviceModels = new ObservableCollection<ServiceModel>();

    public ObservableCollection<ServiceModel> ServiceModels
    {
        get { return _serviceModels; }
    }

} 

I have the following treeview xaml definition:

<TreeView Name="ServiceTree" Grid.Row="1" ItemsSource="{Binding Services}">
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
            <Setter Property="FontWeight" Value="Normal" />
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="FontWeight" Value="Bold" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </TreeView.ItemContainerStyle>
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding}">
            <TextBlock Text="{Binding Name}"/>
            <HierarchicalDataTemplate.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Messages}"/>
                </DataTemplate>
            </HierarchicalDataTemplate.ItemTemplate>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

Nothing is being output in the tree. I have tried following several tutorials on hierarchical data-binding but I'm simply having difficulty understanding the proper technique for my specific situation.

Also, how should the data-context be set? I am setting the context as follows within the code-behind of the view:

public partial class ServiceView : UserControl
{

    private readonly ServiceCollectionViewModel _serviceCollection = new ServiceCollectionViewModel();

    public ObservableCollection<ServiceModel> Services
    {
        get { return _serviceCollection.ServiceModels; }
    }

    public ServiceView()
    {
        InitializeComponent();

        DataContext = _serviceCollection;
        _serviceCollection.LoadServices();

    }
} 
2

There are 2 best solutions below

1
On BEST ANSWER

You seem to be very confused. You're trying to data bind to your ServiceView.Services property, so what is the ServiceCollectionViewModel class for? The sole purpose of a view model is to provide all of the data (and functionality) required for its view.

Now we can go different routes... it all depends what you actually want. If you want to data bind from outside the UserControl to the ServiceView.Services property, then you must declare the Services property as a DependencyProperty. Inside your UserControl, you would then data bind to the property using a RelativeSource Binding like this:

Outside:

<YourPrefix:ServiceView Services="{Binding YourExternalViewModelProperty}" />

Inside:

<TreeView Name="ServiceTree" Grid.Row="1" ItemsSource="{Binding Services, 
    RelativeSource={RelativeSource AncestorType={x:Type YourPrefix:ServiceView}}}">
    ...
</TreeView>

With this method, there is no need to set the DataContext to anything as we are using RelativeSource to set the data source. Alternatively, you could set the DataContext to either the internal UserControl code behind, or an instance of a view model (from either inside or outside) and then normally data bind like this:

<TreeView Name="ServiceTree" Grid.Row="1" ItemsSource="{Binding Services}">
    ...
</TreeView>

So, to recap, if you set the DataContext to an instance of an object, then your Binding Paths look at the properties in that object to resolve themselves. Otherwise, if you set a RelativeSource (or ElementName) in your Binding Path, then you are changing the data source for that Binding only and your Binding Paths should be properties from those objects instead.


UPDATE >>>

The HierarchicalDataTemplate Class is basically a slight extension on the regular old DataTemplate class and can be thought of a DataTemplate with an ItemsSource property. Therefore, if you can define the content of a DataTemplate, then you can define the content of a HierarchicalDataTemplate... just make sure to set the HierarchicalDataTemplate.ItemsSource property to a valid collection property... in your case, the Messages property:

<TreeView.ItemTemplate>
    <HierarchicalDataTemplate DataType="{x:Type YourPrefix:ServiceModel}" 
        ItemsSource="{Binding Messages}"> <!-- Collection Property In ServiceModel -->
        <TextBlock Text="{Binding Name}" /> <!-- Property In ServiceModel -->
    </HierarchicalDataTemplate>
</TreeView.ItemTemplate>

If you're not too clear on DataTemplate, take a look at the Data Binding Overview page on MSDN.

4
On

You have a number of issues here.

  1. ItemsSource binding should be to ServiceModels and not Services, because that is the name of the property in your ServiceCollectionViewModel. SO this way you get the top-level items.

  2. You are trying to bind inside your items' style to IsExpanded and IsSelected properties, but there are no such properties on your view-models.

  3. Inside your HierarchicalDataTemplate you bind the ItemsSource property directly to the DataContext of the TreeViewItem which in this case is ServiceModel. Either make the ServiceModel implement IEnumerable<string> exposing your messages, or bind to Messages property directly:

  4. The second level HierarchicalDataTemplate binds the Text property to Messages, but the DataContext of this level is of String type, having no Messages property. So here you should bind to the data context itself:

Regarding the data context, you better initialize it in XAML, using static resource. Also, much cleaner approach to define the hierarchical data templates is using implicit templates, based on the item types. Because you really don't want to define hierarchical templates inline for a greater number of hierarchy levels (say: 4, 5). See below the completely fixed XAML:

<Grid>
    <Grid.Resources>
        <!--Data context for the whole Grid-->
        <myNamespace:ServiceCollectionViewModel x:Key="MyViewModel" />
    </Grid.Resources>
    <TreeView Name="ServiceTree"
              DataContext="{StaticResource MyViewModel}"
              ItemsSource="{Binding ServiceModels}">
        <TreeView.Resources>

            <!--Implicit style for TreeViewItem, IsExpanded and IsSelected have no binding because 
            they are not used by view-models-->
            <Style TargetType="{x:Type TreeViewItem}">
                <Setter Property="FontWeight" Value="Normal" />
                <Style.Triggers>
                    <Trigger Property="IsSelected" Value="True">
                        <Setter Property="FontWeight" Value="Bold" />
                    </Trigger>
                </Style.Triggers>
            </Style>

            <!--Template for the first level items-->
            <HierarchicalDataTemplate DataType="{x:Type myNamespace:ServiceModel}" ItemsSource="{Binding Messages}">
                <TextBlock Text="{Binding Name}"/>
            </HierarchicalDataTemplate>

            <!--Template for the second level, due to the fact that String is not hierarchical
            regular DataTemplate is enough-->
            <DataTemplate DataType="{x:Type system:String}">
                <TextBlock Text="{Binding}"/>
            </DataTemplate>

        </TreeView.Resources>
    </TreeView>
</Grid>

I believe this one is both more understandable and, consequently, more maintainable.