none
Binden der ScrollIntoView-Eigenschaft eines DataGrid mit MVVM. RRS feed

  • Frage

  • Hallo alle zusammen.

    Ich habe meine ersten Versuche mit MVVM hinter mir. Habe mir hauptsächlich die Command-Bindung angeschaut. Hat auch, dank einiger hilfsbereiter Forummitgliedern recht fein geklappt.

    In einem Beispiel gab es dann auch Eigenschafts-Bindung an ein Datenobjekt aus dem ViewModel. Und natürlich wollte ich mich für meinen ersten Versuch nicht mit dem Binden der Text-Eigenschaft einer TextBox begnügen. Nein, ich musste ja mal wieder übertreiben und mir etwas komplizierteres aussuchen! Ja und jetzt brauch ich eure Hilfe wieder einmal....

    Es geht mir darum die Eigenschaft "Scrollintoview" eines Datagrid-Elements an das "current-Item" meines ViewModels zu binden. Auf meinem Mainwindow befindet sich nämlich unter anderem ein "MoveCurrentToLastItem-Button"  um zum letzten ViewModel-Eintrag zu "springen". Das Problem ist nur, wenn "MoveCurrentToLast" meiner ICollectionview ausgeführt wird, dann sehe ich den Eintrag nicht mehr in meinem DataGrid! Es war allerdings schon sehr schnell vorbei mit der Freude als ich gleich am Anfang meines Versuches feststellen musste, dass es im XAML-Code gar keine ScrollIntoView-Eigenschaft für das Datagrid-Element gibt.

    Weiss vieleicht jemand wie ich mein kleines Problemchen, unter berücksichtigung der MVVM-Prinzipien, lösen kann?

    Danke,

    Basicus

    Samstag, 23. Juli 2011 16:16

Antworten

  • Hallo nochmal,

    ein AttachedBehavior könnte so aussehen:

    Public Class DataGrid_Behavior
    
    #Region " --- DependencyProperties --- "
    
    	''' <summary>
    	''' Gets/sets whether or not the DataGrid this behavior is being applied to 
    	''' automatically scrolls the selected item into view.
    	''' </summary>
    	Public Shared ReadOnly SelectedItem_ScrollIntoViewProperty As DependencyProperty =
    		DependencyProperty.RegisterAttached(
    		"SelectedItem_ScrollIntoView",
    		GetType(Boolean),
    		GetType(DataGrid_Behavior),
    		New UIPropertyMetadata(False, AddressOf OnSelectedItem_ScrollIntoView_Changed)
    	 )
    	' DP-getter and -setter
    	Public Shared Function GetSelectedItem_ScrollIntoView(dg As DataGrid) As Boolean
    		Return DirectCast(dg.GetValue(SelectedItem_ScrollIntoViewProperty), Boolean)
    	End Function
    	Public Shared Sub SetSelectedItem_ScrollIntoView(dg As DataGrid, value As Boolean)
    		dg.SetValue(SelectedItem_ScrollIntoViewProperty, value)
    	End Sub
    
    	''' <summary>
    	''' Fired when the assignment of the behavior changes (IOW, is being turned on or off).
    	''' </summary>
    	Shared Sub OnSelectedItem_ScrollIntoView_Changed(doSource As DependencyObject, e As DependencyPropertyChangedEventArgs)
    		' The DataGrid that is the target of the assignment
    		Dim dg = TryCast(doSource, System.Windows.Controls.DataGrid)
    		If dg Is Nothing Then
    			Throw New Exception("Behavior must be attached to a DataGrid!")
    		End If
    
    		' Just to be safe ...
    		If TypeOf e.NewValue Is Boolean = False Then
    			Return
    		End If
    
    		If DirectCast(e.NewValue, Boolean) Then
    			' Attach
    			AddHandler dg.SelectionChanged, AddressOf OnSelectionChanged
    		Else
    			' Detach
    			RemoveHandler dg.SelectionChanged, AddressOf OnSelectionChanged
    		End If
    	End Sub
    
    #End Region	' <--- DependencyProperties ---
    
    #Region " --- DataGrid-events (consumed) --- "
    
    	Shared Sub OnSelectionChanged(sender As Object, e As System.Windows.Controls.SelectionChangedEventArgs)
    		Dim dg = DirectCast(sender, System.Windows.Controls.DataGrid)
    
    		If e.AddedItems.Count > 0 AndAlso dg.SelectedItem IsNot Nothing Then
    			dg.ScrollIntoView(dg.SelectedItem)
    		End If
    	End Sub
    
    #End Region	' <--- DataGrid-events (consumed) ---
    
    End Class
    

    Und hier noch ein Test-Window dazu:

    <Window x:Class="DataGrid_ScrollIntoViewBehavior"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfTests_VB4"
        Title="DataGrid_ScrollIntoViewBehavior" Height="300" Width="300">
      <DockPanel>
       <Button Content="Select last item in the DataGrid"
           Margin="5" Padding="15,3"
           DockPanel.Dock="Bottom"
           Click="Button_Click"/>
       
       <DataGrid x:Name="dg" 
            ItemsSource="{Binding ColData}"
            AutoGenerateColumns="False"
            local:DataGrid_Behavior.SelectedItem_ScrollIntoView="True">
         <DataGrid.Columns>
          <DataGridTextColumn Header="Dummy" Binding="{Binding}" Width="*"/>
         </DataGrid.Columns>
       </DataGrid>
      </DockPanel>
    </Window>
    
    

    Code-behind des Windows:

    Option Strict On
    Option Explicit On
    
    Imports System.Collections.ObjectModel
    
    Public Class DataGrid_ScrollIntoViewBehavior
    
    	Public Property ColData As ObservableCollection(Of String)
    
    	Public Sub New()
    		InitializeComponent()
    
    		ColData = New ObservableCollection(Of String)
    		Me.DataContext = Me
    		For intCounter As Integer = 1 To 100
    			ColData.Add("Item #" & intCounter.ToString())
    		Next
    	End Sub
    
    	Private Sub Button_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
    		If dg.HasItems Then dg.SelectedIndex = dg.Items.Count - 1
    	End Sub
    
    End Class
    
    

     

    Was den Vorschlag mit Events anbelangt: Hier würde das VM ein RaiseEvent ausführen, wenn der SelectedItem geändert wird. Das wiederum würdest Du im V abfangen, sprich, Du würdest Dein VM per WithEvents deklarieren und dann Code in den entspr. Event packen.

    Das AttachedBehavior eigentlich finde ich aber eigentlich netter (und sinnvoller, da allgemeiner). :-)
    Außerdem könnte man das Behavior auch noch so umschreiben/erweitern, dass es auch andere Typen berücksichtigt, z.B. ListView, ListBox, etc..


    Cheers,
    Olaf
    http://blogs.intuidev.com
    • Als Antwort markiert Basicus Mittwoch, 27. Juli 2011 15:55
    Dienstag, 26. Juli 2011 16:06

Alle Antworten

  • ScrollIntoView ist eine Methode. Um sie auszuführen, benötigt man einen Verweis auf das DataGrid. Diesen Verweis kann man bei Nutzung von MVVM über eine Attached Property bekommen.
     
    --
    Viele Gruesse
    Peter
    Samstag, 23. Juli 2011 18:07
  • Oooops! Ja stimmt Scrollintoview ist eine Methode und keine Eigenschaft.

    Trotz deiner Hinweise konnte ich dein kleines "Rätsel" noch nicht lösen. Ich hab zwar eine Lösung falls ich es nicht rausfinden sollte. Wenn ich im ViewModel FindName("ElementName") aufrufe dann hätte ich zwar den benötigten Verweis auf das DataGrid, aber dann wäre leider mein MVVM im "Eimer"! Und das will ich ja vermeiden.

    Hier mal meine Gedanken. MoveCurrentToLast meiner ICollectionView wird ja im VM ausgeführt. Und mein VM darf ja nicht eimal "wissen", dass es eine View auf Mainwindow gibt. Im VM könnte man eine Public Readonly Property hinzufügen um Mainwindow (oder anderen Klassen) mitzuteilen dass MoveCurrentToLast ausgeführt worden ist. Denn die "Handlung" wird ja wohl im Mainwindow stattfinden müssen. Denn Mainwindow "kennt" das VM ja. (das VM ist im Mainwiindow referenziert) Dann muss ja wohl ein Event oder ein Trigger dem DataGrid mitteilen wenn ScrollIntoView ausgeführt werden soll..... Naja blicke da nicht ganz durch. :-(

    Könntest du mir bitte den richtigen Gedankengang verraten. Welche Komponente an dem Vorgang beteiligt sind?

    Danke,

    Basicus

    Sonntag, 24. Juli 2011 08:10
  • Hallo auch,

    der Punkt ist doch, dass der Wunsch, einen Item für Benutzer sichtbar zu machen, klar eine Eigenschaft des Views ist. Stell Dir doch einfach vor, dass Dein VM auch mit einer anderen UI funktionieren kann, in der dies ganz anders gehandhabt wird. Insofern ist dies ein typisches Beispiel für die Notwendigkeit von Code im View selbst.


    Cheers,
    Olaf
    http://blogs.intuidev.com
    Sonntag, 24. Juli 2011 10:21
  • Hallo Olaf,

    ja deine Argumente sind überzeugend. Dann bedeutet es deiner Meinung nach also keinen Verstoss gegen die MVVM-Prinzipien die ScrollIntoView-Methode des DataGrids im Codebenind der Mainwindow, und nicht im ViewModel auszuführen?

    Danke,

    Basicus

     

    Montag, 25. Juli 2011 14:31
  • Hallo auch,
    Dann bedeutet es deiner Meinung nach also keinen Verstoss gegen die MVVM-Prinzipien die ScrollIntoView-Methode des DataGrids im Codebenind der Mainwindow, und nicht im ViewModel auszuführen?

    exactamundo. Du rufst ja schließlich auch eine Methode auf, die DataGrid-spezifisch ist. Selbst wenn ScrollIntoView eine Methode des ItemsControls wäre (und somit auf höherer Ebene verfügbar), hätte sie dennoch im VM nichts verloren, da sich dies eindeutig auf die Darstellung bzw. Visualisierung handelt.

    Das sind jedenfalls meine €0.02. :-)

    Cheers,
    Olaf
    http://blogs.intuidev.com
    Montag, 25. Juli 2011 17:37
  • Hi Olaf,
    wenn ScrollIntoView eine direkte synchrone Folge einer Bedieneraktivität ist, dann bin ich mit Deiner Meinung einverstanden. Wenn ScrollIntoView aber erforderlich ist, weil sich im ViewModel etwas verändert hat, dann sollte ScrollIntoView auch direkt aus dem ViewModel aufgerufen werden. Den Verweis kann sich der ViewModel über eine Attached Property holen.
    Ein Beispiel ist das asynchrone Laden von Daten, wie es in Silverlight Anwendungen üblich ist. Wenn der Aufruf der ScrollIntoView Methode in diesem Fall in der Oberfläche ausgeführt werden soll, dann muss der ViewModel die Oberfläche kennen, z.B. über ein Interface. Damit wird die Bindung fester und Vorteile von MVVM können teilweise verloren gehen, was sich in erschwerten Tests äußern kann.
    --
    Viele Gruesse
    Peter
    Dienstag, 26. Juli 2011 03:52
  • Hi Peter,

    bin grundsätzlich ganz Deiner Meinung. Es kommt halt darauf an, ob man M und VM auch mit anderen Technologien zu verknüpfen plant (z.B. ASP.Net als alternativer Oberfläche) - dann hat man gar keine andere Möglichkeit, als das VM von solchen Geschichten freizuhalten. Prinzipiell sollte sich ein ScrollIntoView, wie von Dir geschildert, aber auch auslösen lassen, indem das VM einen Event feuert, den der View verwendet.

    Ich persönlich mag MVVM vor allem auch, weil es mir u.a. mittels Vererbung ermöglicht, eine Basis für gleichartig zu verarbeitende Datengruppen herzustellen und nur noch den (gruppen- bzw. tabellen-) spezifischen Teil "auszuprogrammieren". Dabei habe ich - je nach Projekt - noch nie ein Problem gehabt, vom Pattern abzuweichen. Für mich ist das aber nur ein Grundkonzept, nicht mehr. Sprich, ich habe mit Abweichungen nur in besonderen Fällen, bzw. falls das VM auch mit einer anderen Technologie verwendet werden soll, ein Problem.


    Cheers,
    Olaf
    http://blogs.intuidev.com
    Dienstag, 26. Juli 2011 07:43
  • Hallo Olaf, hallo Peter. Leider fehlt mir das nötige Wissen und die Erfahrung um in euerer Diskussion mitreden zu können.

    Aber jetzt wo ich Peters Argument gehört habe, finde ich das auch sehr logisch. Ich will es also über den Weg der Attached Properties versuchen.

    Aber leider bin ich noch auf keinen grünen Zweig gekommen. Das einzige was ich bis jetzt im Internet finden konnte ist ein ähnliches Problem. Aber mit einem TreeView-Element statt mit einem DataGrid. Beim TreeView scheint die Methode "BringIntoView" einen ähnlichen Zweck wie "Scrollintoview" biem DataGrid zu erfüllen. Ich kann den Link hier nicht posten, da er von einem anderen Forum stammt.(verstösst doch sicher gegen die Regeln) Dieser Mann redet von "Attached behaviour" anstatt von "Attached Properties". Nehme aber an, dass er das damit meint. Der Code ist leider auch in C# geschrieben. Und so kann ich leider nichts damit anfangen. Er ist ausserdem auch nicht vollständig gepostet worden.

    Wenn mir also bitte jemand auf die Sprünge helfen könnte um diesen Verweis auf das Datagrid, im ViewModel, mittels attached properties herzustellen, das würde mir sehr helfen.

    Danke,

    Basicus

    Dienstag, 26. Juli 2011 10:57
  • "Attached behaviour" ist der richtige Ausdruck. Ich hatte da gedankenlos etwas falsch formuliert. Aber auch Olaf’s Verfahrensweise mit Ereignissen ist eine gute Lösung.
     
    --
    Viele Gruesse
    Peter
    Dienstag, 26. Juli 2011 11:36
  • Ist ja schon fast beruhigend zu sehen, dass auch andere mal Fehler machen... ;-)

    Ich hab ja auch nichts gegen Olafs Vorschlag. Doch da brauch ich im Ereignis (Event) des VM doch aber auch einen Verweis auf das DataGrid im View? (also im MainWindow) Und leider finde ich nicht heraus wie das geht! Das geht ja dann bestimmt auch wieder über diese "attached behaviours"? Oder sitze ich total auf der Leitung?

    Danke,

    Basicus

     

    Dienstag, 26. Juli 2011 12:29
  • Hallo nochmal,

    ein AttachedBehavior könnte so aussehen:

    Public Class DataGrid_Behavior
    
    #Region " --- DependencyProperties --- "
    
    	''' <summary>
    	''' Gets/sets whether or not the DataGrid this behavior is being applied to 
    	''' automatically scrolls the selected item into view.
    	''' </summary>
    	Public Shared ReadOnly SelectedItem_ScrollIntoViewProperty As DependencyProperty =
    		DependencyProperty.RegisterAttached(
    		"SelectedItem_ScrollIntoView",
    		GetType(Boolean),
    		GetType(DataGrid_Behavior),
    		New UIPropertyMetadata(False, AddressOf OnSelectedItem_ScrollIntoView_Changed)
    	 )
    	' DP-getter and -setter
    	Public Shared Function GetSelectedItem_ScrollIntoView(dg As DataGrid) As Boolean
    		Return DirectCast(dg.GetValue(SelectedItem_ScrollIntoViewProperty), Boolean)
    	End Function
    	Public Shared Sub SetSelectedItem_ScrollIntoView(dg As DataGrid, value As Boolean)
    		dg.SetValue(SelectedItem_ScrollIntoViewProperty, value)
    	End Sub
    
    	''' <summary>
    	''' Fired when the assignment of the behavior changes (IOW, is being turned on or off).
    	''' </summary>
    	Shared Sub OnSelectedItem_ScrollIntoView_Changed(doSource As DependencyObject, e As DependencyPropertyChangedEventArgs)
    		' The DataGrid that is the target of the assignment
    		Dim dg = TryCast(doSource, System.Windows.Controls.DataGrid)
    		If dg Is Nothing Then
    			Throw New Exception("Behavior must be attached to a DataGrid!")
    		End If
    
    		' Just to be safe ...
    		If TypeOf e.NewValue Is Boolean = False Then
    			Return
    		End If
    
    		If DirectCast(e.NewValue, Boolean) Then
    			' Attach
    			AddHandler dg.SelectionChanged, AddressOf OnSelectionChanged
    		Else
    			' Detach
    			RemoveHandler dg.SelectionChanged, AddressOf OnSelectionChanged
    		End If
    	End Sub
    
    #End Region	' <--- DependencyProperties ---
    
    #Region " --- DataGrid-events (consumed) --- "
    
    	Shared Sub OnSelectionChanged(sender As Object, e As System.Windows.Controls.SelectionChangedEventArgs)
    		Dim dg = DirectCast(sender, System.Windows.Controls.DataGrid)
    
    		If e.AddedItems.Count > 0 AndAlso dg.SelectedItem IsNot Nothing Then
    			dg.ScrollIntoView(dg.SelectedItem)
    		End If
    	End Sub
    
    #End Region	' <--- DataGrid-events (consumed) ---
    
    End Class
    

    Und hier noch ein Test-Window dazu:

    <Window x:Class="DataGrid_ScrollIntoViewBehavior"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfTests_VB4"
        Title="DataGrid_ScrollIntoViewBehavior" Height="300" Width="300">
      <DockPanel>
       <Button Content="Select last item in the DataGrid"
           Margin="5" Padding="15,3"
           DockPanel.Dock="Bottom"
           Click="Button_Click"/>
       
       <DataGrid x:Name="dg" 
            ItemsSource="{Binding ColData}"
            AutoGenerateColumns="False"
            local:DataGrid_Behavior.SelectedItem_ScrollIntoView="True">
         <DataGrid.Columns>
          <DataGridTextColumn Header="Dummy" Binding="{Binding}" Width="*"/>
         </DataGrid.Columns>
       </DataGrid>
      </DockPanel>
    </Window>
    
    

    Code-behind des Windows:

    Option Strict On
    Option Explicit On
    
    Imports System.Collections.ObjectModel
    
    Public Class DataGrid_ScrollIntoViewBehavior
    
    	Public Property ColData As ObservableCollection(Of String)
    
    	Public Sub New()
    		InitializeComponent()
    
    		ColData = New ObservableCollection(Of String)
    		Me.DataContext = Me
    		For intCounter As Integer = 1 To 100
    			ColData.Add("Item #" & intCounter.ToString())
    		Next
    	End Sub
    
    	Private Sub Button_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
    		If dg.HasItems Then dg.SelectedIndex = dg.Items.Count - 1
    	End Sub
    
    End Class
    
    

     

    Was den Vorschlag mit Events anbelangt: Hier würde das VM ein RaiseEvent ausführen, wenn der SelectedItem geändert wird. Das wiederum würdest Du im V abfangen, sprich, Du würdest Dein VM per WithEvents deklarieren und dann Code in den entspr. Event packen.

    Das AttachedBehavior eigentlich finde ich aber eigentlich netter (und sinnvoller, da allgemeiner). :-)
    Außerdem könnte man das Behavior auch noch so umschreiben/erweitern, dass es auch andere Typen berücksichtigt, z.B. ListView, ListBox, etc..


    Cheers,
    Olaf
    http://blogs.intuidev.com
    • Als Antwort markiert Basicus Mittwoch, 27. Juli 2011 15:55
    Dienstag, 26. Juli 2011 16:06
  • Hallo Olaf, Dein Code ist einfach nur super und funktioniert wunderbar. Danke!

    Ich muss ehrlich zugeben, dass ich da nicht zu 100% durchblicke, was, wie, weshalb passiert. Aber dank deiner klaren Komentare, weiss ich wenigstens wo, was passiert.

    Also ich nehme schon an, dass ich die Sache wenigstens vom Prinzip her einigermassen verstanden habe. Aber ich waage trotzdem noch ein paar Fragen.

    Es ist ja so dass ich diese DependencyProperty auch auf mehrere DataGrids setzen könnte?

    Für einen weiteres DataGrid, bei dem diese DP nicht erwünscht wäre, da bräuchte man "DataGrid_Behavior.SelectedItem_ScrollIntoView" nicht extra auf "False" zu setzen. Es würde dann doch bestimmt reichen gar nichts anzugeben. Oder?

    Und was ich irgendwie witzig finde, ist, dass das ViewModel weder von der Existenz des DataGrids noch von der DependencyProperty "weiss". Und die DP wiederum kennt nur das DataGrid, und weiss nichts vom VM. Das fasziniert mich als Anfänger. Keiner weiss von dem Anderen, und trotzdem läuft alles. Ist schon cool!

    Danke,

    Basicus

    Mittwoch, 27. Juli 2011 15:55
  • Es ist ja so dass ich diese DependencyProperty auch auf mehrere DataGrids setzen könnte?
     
    Natürlich ist das möglich. Schau die mal genau an, was da gemacht wird.
     
    1. Die ViewModel-Klasse bekommt ein statische Eigenschaft in Form eine Dependency Property (DP). Diese DP hat mit einer konkreten Instanz eines ViewModels nichts zu tun, da sie statisch ist.
     
    2. Zum Zeitpunkt der Instanziierung des Steuerelementes im XAML (in deinem konketen Fall, das Datagrid) wird die DP in der ViewModel-Klasse aufgerufen.
     
    3. Über die formalen Parameter der Signatur der aufgerufenen Methode wird der Verursacher mitgeliefert. Damit steht der statischen Methode der DP der Objektverweis auf das Steuerelement der Oberfläche bereit. Damit hast Du einen Verweis auf das konkrete DataGrid. Mit diesem Verweis kannst Du alle Member nutzen, die das DataGrid öffentlich bereitstellt.
     
    4. Unter Nutzung des Objektverweises auf das DataGrid wird eine Ereignisroutine an das SelectionChanged-Ereignis gebunden.
     
    5. Wenn im DataGrid das SelectionChanged-Ereignis ausgelöst wird, wird über den ersten formalen Parameter der Verursacher geliefert, in Deinem Fall das konkrete DataGrid. Diesen Verweis kann man nutzen, um dann konkrete Methoden im DataGrid aufzurufen, z.B. ScrollIntoView.
     
    --
    Viele Gruesse
    Peter
    Mittwoch, 27. Juli 2011 16:50
  • Hallo nochmal,

    Peters Ausführungen kann ich eigentlich nichts mehr hinzufügen. Außer ...

    Für einen weiteres DataGrid, bei dem diese DP nicht erwünscht wäre, da bräuchte man "DataGrid_Behavior.SelectedItem_ScrollIntoView" nicht extra auf "False" zu setzen. Es würde dann doch bestimmt reichen gar nichts anzugeben. Oder?

    ... dass auch das richtig ist. Der Code für das "Ausschalten" des AB ist prinzipiell nicht notwendig, wenn kein externer Mechanismus existiert, über den das AB zur Laufzeit kontrolliert würde. Soll heißen, wenn Du es nur über XAML zur Designzeit steuerst, könnte man ein paar Zeilen AB-Code sparen, aber das macht ja nicht wirklich Sinn.


    Cheers,
    Olaf
    http://blogs.intuidev.com
    Donnerstag, 28. Juli 2011 05:20