none
WPF MVVM : Treeview Templates und Trigger RRS feed

  • Frage

  • Hallo zusammen,
    nun muss ich auch mal mit neuem GUI zeugs arbeiten ;-)

    Ich will einen TreeView darstellen.

    <TreeView ItemsSource="{Binding Items}">
                <TreeView.ItemContainerStyle>
                    <Style TargetType="{x:Type TreeViewItem}" >
                        <Style.Triggers>
                            <Trigger Property="HasItems" Value="true">
                                <Setter Property="Focusable" Value="False"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </TreeView.ItemContainerStyle>
                    <TreeView.ItemTemplate>
                    <HierarchicalDataTemplate ItemsSource="{Binding SubNodes}">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="20"/>
                                <ColumnDefinition/>
                            </Grid.ColumnDefinitions>
                            <CheckBox Grid.Column="0"/>
                            <TextBlock Text="{Binding Text}" Grid.Column="1"/>
                        </Grid>
                    </HierarchicalDataTemplate>
                </TreeView.ItemTemplate>
            </TreeView>
    

    Das klappt auch alles wunderbar nur :
    Frage 1-> Wie kann ich zB den Items ein anderes template zuweisen ? (Also Items sollen keine Checkbox haben , die SubNodes aber schon)
    Frage 2-> Ich wil lzwei Darstellungsformen : Einmal SubNodes MIT Checkbox und einmal ohne. Bisher habe ich durch Googlen nicht rausgefunden, wie ich zB zwei Templates dem Grid zuweisen kann

    Frage 3. -> Wenn ein SubNode mit Checkbox angehakt wurde, sollen alle anderen SubItems Disabled werden. Bestimmt über Trigger nur wie ist mir gerade rätselhaft.

    Kann mir jmd helfen ?

    Grüße

    Freitag, 2. September 2011 08:49

Antworten

  • Hi Pawel,
    was Du als “neues GUI Zeugs” bezeichnest, empfinde ich als riesigen Fortschritt nach der Pixelarbeit aus PC-Urzeiten, wie die CPU wegen schwachen Grafikkarten die Anzeige noch selbst rendern musste.
     
    Wie ich Deine Frage verstanden habe, willst Du unterschiedlich Knotendarstellungen und Zustandsabhängigkeit zwischen den Knoten.
     
    Für unterschiedliche Knotendarstellungen kannst Du unterschiedliche Vorlagen (templates) nutzen, die sich auf den Typ des gebundenen Datenelementes beziehen. Das bedeutet, dass in den Ausgangsdaten für jede Knotendarstellung ein anderer Typ zu nutzen ist. Der Einfachkeit halber kann man diese Typen von einem Basistyp ableiten.
     
    In den Datenelementen kann man eine Eltern-Beziehung verwalten, über die man den Zustand des Elternelementes auswertet und dementsprechend die Anzeige beeinflusst.
     
    Nachfolgend habe ich mal eine Demo zu Deiner Frage aufgebaut:
    <Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        xmlns:local="clr-namespace:WpfApplication1">
      <Window.Resources>
        <local:ViewModel x:Key="vm"/>
      </Window.Resources>
      <Grid DataContext="{Binding Source={StaticResource vm}}">
        <TreeView ItemsSource="{Binding Items}">
          <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}" >
              <Style.Triggers>
                <Trigger Property="HasItems" Value="true">
                  <Setter Property="Focusable" Value="False"/>
                </Trigger>
              </Style.Triggers>
            </Style>
          </TreeView.ItemContainerStyle>
          <TreeView.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:VMItem}"
                                  ItemsSource="{Binding Path=SubNodes}">
              <Grid>
                <TextBlock Text="{Binding Text}" />
              </Grid>
            </HierarchicalDataTemplate>
            <HierarchicalDataTemplate DataType="{x:Type local:VMSubItem}"
                                  ItemsSource="{Binding Path=SubNodes}">
              <Grid IsEnabled="{Binding SubNodeEnabled}">
                <Grid.ColumnDefinitions>
                  <ColumnDefinition Width="20"/>
                  <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <CheckBox Grid.Column="0" IsChecked="{Binding IsChecked, Mode=TwoWay}"/>
                <TextBlock Text="{Binding Text}" Grid.Column="1"/>
              </Grid>
            </HierarchicalDataTemplate>
          </TreeView.Resources>
        </TreeView>
      </Grid>
    </Window>
    

    Der ViewModel dazu:

    Imports System.Collections.ObjectModel
    Imports System.ComponentModel
    
    Public Class ViewModel
    
      Private m As New Model
      Dim cvs As New CollectionViewSource
    
      Public ReadOnly Property Items() As ICollectionView
        Get
          If cvs.View Is Nothing Then cvs.Source = GetItems()
          Return cvs.View
        End Get
      End Property
    
      Private Function GetItems() As ObservableCollection(Of VMItem)
        Dim liste As New ObservableCollection(Of VMItem)
        For Each r In m.Data(0)
          Dim itm1 = New VMItem() _
                        With {.ID = CType(r!ID, Integer), _
                              .Text = r!desc.ToString}
          liste.Add(itm1)
          itm1.SubNodes = GetSubItems(itm1)
        Next
        Return liste
      End Function
    
      Private Function GetSubItems(ByVal itm0 As VMItem) As ObservableCollection(Of VMSubItem)
        Dim liste As New ObservableCollection(Of VMSubItem)
        For Each r In m.Data(itm0.ID)
          Dim itm1 = New VMSubItem() _
                        With {.ID = CType(r!ID, Integer), _
                              .Text = r!desc.ToString, _
                              .Parent = itm0}
    
          liste.Add(itm1)
          itm1.SubNodes = GetSubItems(itm1)
        Next
        Return liste
      End Function
    
    End Class
    
    Public Class VMItem
    
      Public Property ID() As Integer
      Public Property Text() As String
      Public Property SubNodes() As ObservableCollection(Of VMSubItem)
    
    End Class
    
    Public Class VMSubItem
      Inherits VMItem
      Implements INotifyPropertyChanged
    
      Public Property Parent() As VMItem
    
      Private _isChecked As Boolean = False
      Public Property IsChecked As Boolean
        Get
          Return Me._isChecked
        End Get
        Set(value As Boolean)
          If value <> Me._isChecked Then
            Me._isChecked = value
            ChangeSubNodeEnabled()
          End If
        End Set
      End Property
    
      Friend Sub ChangeSubNodeEnabled()
        For Each n In Me.SubNodes
          n.OnPropertyChanged("SubNodeEnabled")
          n.ChangeSubNodeEnabled()
        Next
      End Sub
    
      Public ReadOnly Property SubNodeEnabled As Boolean
        Get
          If TypeOf Parent Is VMSubItem Then
            With CType(Parent, VMSubItem)
              Return (Not .IsChecked) And .SubNodeEnabled
            End With
          Else
            Return True
          End If
        End Get
      End Property
    
    #Region " PropertyChanged"
      Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
      Friend Sub OnPropertyChanged(ByVal prop As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(prop))
      End Sub
    #End Region
    
    End Class
    

    Die zufälligen Demodaten generiere ich so:

    Imports System.Data
    
    Public Class Model
    
      Private dt As New DataTable
    
      Public Sub New()
        Dim rnd As New Random
        With dt
          With ..Columns
            .Add("ID", GetType(Integer))
            .Add("FK", GetType(Integer))
            .Add("Desc", GetType(String))
          End With
          For i = 1 To 10000
            Dim r = .NewRow
            r!ID = i
            r!FK = rnd.Next(0, i - 1)
            r!Desc = "Item " & i.ToString
            .Rows.Add(r)
          Next
        End With
      End Sub
    
      Public ReadOnly Property Data(ByVal fk As Integer) As DataRow()
        Get
          Return Me.dt.Select("FK = " & fk.ToString)
        End Get
      End Property
    
    End Class
    

    --
    Viele Gruesse
    Peter

    • Als Antwort vorgeschlagen Peter Fleischer Samstag, 3. September 2011 08:23
    • Als Antwort markiert Pawel Warmuth Samstag, 3. September 2011 13:00
    Samstag, 3. September 2011 08:13

Alle Antworten

  • Hi Pawel,
    was Du als “neues GUI Zeugs” bezeichnest, empfinde ich als riesigen Fortschritt nach der Pixelarbeit aus PC-Urzeiten, wie die CPU wegen schwachen Grafikkarten die Anzeige noch selbst rendern musste.
     
    Wie ich Deine Frage verstanden habe, willst Du unterschiedlich Knotendarstellungen und Zustandsabhängigkeit zwischen den Knoten.
     
    Für unterschiedliche Knotendarstellungen kannst Du unterschiedliche Vorlagen (templates) nutzen, die sich auf den Typ des gebundenen Datenelementes beziehen. Das bedeutet, dass in den Ausgangsdaten für jede Knotendarstellung ein anderer Typ zu nutzen ist. Der Einfachkeit halber kann man diese Typen von einem Basistyp ableiten.
     
    In den Datenelementen kann man eine Eltern-Beziehung verwalten, über die man den Zustand des Elternelementes auswertet und dementsprechend die Anzeige beeinflusst.
     
    Nachfolgend habe ich mal eine Demo zu Deiner Frage aufgebaut:
    <Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        xmlns:local="clr-namespace:WpfApplication1">
      <Window.Resources>
        <local:ViewModel x:Key="vm"/>
      </Window.Resources>
      <Grid DataContext="{Binding Source={StaticResource vm}}">
        <TreeView ItemsSource="{Binding Items}">
          <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}" >
              <Style.Triggers>
                <Trigger Property="HasItems" Value="true">
                  <Setter Property="Focusable" Value="False"/>
                </Trigger>
              </Style.Triggers>
            </Style>
          </TreeView.ItemContainerStyle>
          <TreeView.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:VMItem}"
                                  ItemsSource="{Binding Path=SubNodes}">
              <Grid>
                <TextBlock Text="{Binding Text}" />
              </Grid>
            </HierarchicalDataTemplate>
            <HierarchicalDataTemplate DataType="{x:Type local:VMSubItem}"
                                  ItemsSource="{Binding Path=SubNodes}">
              <Grid IsEnabled="{Binding SubNodeEnabled}">
                <Grid.ColumnDefinitions>
                  <ColumnDefinition Width="20"/>
                  <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <CheckBox Grid.Column="0" IsChecked="{Binding IsChecked, Mode=TwoWay}"/>
                <TextBlock Text="{Binding Text}" Grid.Column="1"/>
              </Grid>
            </HierarchicalDataTemplate>
          </TreeView.Resources>
        </TreeView>
      </Grid>
    </Window>
    

    Der ViewModel dazu:

    Imports System.Collections.ObjectModel
    Imports System.ComponentModel
    
    Public Class ViewModel
    
      Private m As New Model
      Dim cvs As New CollectionViewSource
    
      Public ReadOnly Property Items() As ICollectionView
        Get
          If cvs.View Is Nothing Then cvs.Source = GetItems()
          Return cvs.View
        End Get
      End Property
    
      Private Function GetItems() As ObservableCollection(Of VMItem)
        Dim liste As New ObservableCollection(Of VMItem)
        For Each r In m.Data(0)
          Dim itm1 = New VMItem() _
                        With {.ID = CType(r!ID, Integer), _
                              .Text = r!desc.ToString}
          liste.Add(itm1)
          itm1.SubNodes = GetSubItems(itm1)
        Next
        Return liste
      End Function
    
      Private Function GetSubItems(ByVal itm0 As VMItem) As ObservableCollection(Of VMSubItem)
        Dim liste As New ObservableCollection(Of VMSubItem)
        For Each r In m.Data(itm0.ID)
          Dim itm1 = New VMSubItem() _
                        With {.ID = CType(r!ID, Integer), _
                              .Text = r!desc.ToString, _
                              .Parent = itm0}
    
          liste.Add(itm1)
          itm1.SubNodes = GetSubItems(itm1)
        Next
        Return liste
      End Function
    
    End Class
    
    Public Class VMItem
    
      Public Property ID() As Integer
      Public Property Text() As String
      Public Property SubNodes() As ObservableCollection(Of VMSubItem)
    
    End Class
    
    Public Class VMSubItem
      Inherits VMItem
      Implements INotifyPropertyChanged
    
      Public Property Parent() As VMItem
    
      Private _isChecked As Boolean = False
      Public Property IsChecked As Boolean
        Get
          Return Me._isChecked
        End Get
        Set(value As Boolean)
          If value <> Me._isChecked Then
            Me._isChecked = value
            ChangeSubNodeEnabled()
          End If
        End Set
      End Property
    
      Friend Sub ChangeSubNodeEnabled()
        For Each n In Me.SubNodes
          n.OnPropertyChanged("SubNodeEnabled")
          n.ChangeSubNodeEnabled()
        Next
      End Sub
    
      Public ReadOnly Property SubNodeEnabled As Boolean
        Get
          If TypeOf Parent Is VMSubItem Then
            With CType(Parent, VMSubItem)
              Return (Not .IsChecked) And .SubNodeEnabled
            End With
          Else
            Return True
          End If
        End Get
      End Property
    
    #Region " PropertyChanged"
      Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
      Friend Sub OnPropertyChanged(ByVal prop As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(prop))
      End Sub
    #End Region
    
    End Class
    

    Die zufälligen Demodaten generiere ich so:

    Imports System.Data
    
    Public Class Model
    
      Private dt As New DataTable
    
      Public Sub New()
        Dim rnd As New Random
        With dt
          With ..Columns
            .Add("ID", GetType(Integer))
            .Add("FK", GetType(Integer))
            .Add("Desc", GetType(String))
          End With
          For i = 1 To 10000
            Dim r = .NewRow
            r!ID = i
            r!FK = rnd.Next(0, i - 1)
            r!Desc = "Item " & i.ToString
            .Rows.Add(r)
          Next
        End With
      End Sub
    
      Public ReadOnly Property Data(ByVal fk As Integer) As DataRow()
        Get
          Return Me.dt.Select("FK = " & fk.ToString)
        End Get
      End Property
    
    End Class
    

    --
    Viele Gruesse
    Peter

    • Als Antwort vorgeschlagen Peter Fleischer Samstag, 3. September 2011 08:23
    • Als Antwort markiert Pawel Warmuth Samstag, 3. September 2011 13:00
    Samstag, 3. September 2011 08:13
  • Hallo Peter,

    ich wollte keinesfalls abfällig gegenüber WPF sein, ich finde die Paradigmen echt super. Leider wird das Thema MVVM nur sehr Stiefmütterlich behandelt seitens MS.

    Noch zwei Fragen zu deiner Lösung : Ist es nicht so, dass die View vom Model nichts willsen sollte ? Durch das abfragen des Typs koppel ich doch den View wieder an das Model oder ? Durch deine Lösung kam mir da die Idee , einfach ein ITreeView Interface zu bauen , welches je nach Typ im ViewModel in den MainView injiziert wird. Meinst du das geht ?

    Und die Frage mit den Checkboxen (Frage 3. -> Wenn ein SubNode mit Checkbox angehakt wurde, sollen alle anderen SubItems Disabled werden. Bestimmt über Trigger nur wie ist mir gerade rätselhaft.) konnte ich bisher leider gar nicht beantworten :-(

    Hast du da noch Ideen bzw. Anregungen ?

    Grüße

    Pawel

    Samstag, 3. September 2011 09:31
  • Hi Pawel,
    ich finde nicht, dass MVVM stiefmütterlicher behandelt als andere OOP-Techniken.
     
    In meinem Beispiel weiß der View auch nichts vom Model. In meinem Beispiel wird im ViewModel ein Mapping der Datenobjekte durchgeführt wird.
     
    Alternativ kann man natürlich auch in allen 3 Schichten die gleichen Objekte nutzen, die vom Model gefüllt und im View angezeigt werden. Die Deklarationen der Typen dazu kann man in eine gemeinsam genutzte Bibliothek auslagern. Das widerspricht nicht der funktionellen Teilung in View – ViewModel – Model.
     
    Auch wenn Du ein ITreeView deklarierst, bleibt die Frage, wie Du die Daten dafür zuordnest. Dein Objekt, welches ITreeView implementiert, muss irgendwie an die vom Model bereitgestellten Daten kommen. Entweder Du führst im ViewModel ein Mapping durch oder Du nutzt die vom Model bereitgestellten Datenobjekte.
     
    Was Du mit dem Interface erreichen willst, kann ich nicht erkennen. Die Oberfläche holt sich die Daten aus dem ViewModel. Wenn sich etwas im ViewModel ändert, wird dies der Oberfläche über NotifyPropertyChanged mitgeteilt und die Oberfläche holt sich die neuen/geänderten Daten.
     
    Die 3. Frage ist in meinem Beispiel beantwortet, indem IsEnabled an die Kette der Eltern-Elemente gebunden ist. Mit dem Ändern einer ChechBox werden alle Kinder und Kindeskinder informiert und über den Notify-Mechanismus wird die Oberfläche zum Aktualisieren aufgefordert. Ich vermute, dass Du Dir mein Beispiel noch gar nicht richtig angeschaut und ausgeführt hast.
     
     
    --
    Viele Gruesse
    Peter
    Samstag, 3. September 2011 10:39
  • Hallo Peter,

    Asche über mein Haupt, ich habe tatsächlich nicht bis ganz nach unten gescrollt !

    Vielen Dank für die Antort und ein schönes Wochenende !

    Grüße

    Pawel
    Samstag, 3. September 2011 13:01