MVVM: User-Auswahl im Viewmodel empfangen

    • WPF

    Es gibt 2 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      MVVM: User-Auswahl im Viewmodel empfangen

      Eine typische Gui-Funktionalität ist das Auswählen aus Listen, und die getroffene Auswahl soll dann im Viewmodel weiterverarbeitet werden.
      Hier mal ein PictureViewer:

      FileInfos werden links in einer Listbox präsentiert, und aus dem ausgewählte FileInfo wird eine ImageSource erstellt und angezeigt. Ausserdem sind Name und Größe der angewählten Datei angezeigt, aber auch die Abmaße der ImageSource (was komplizierterweise keine Eigenschaft des FileInfos ist).

      Nun kann man zwar im Viewmodel eine Auflistung instanzieren, und im Xaml auch daran binden (etwa Listbox.ItemsSource an eine ObservableCollection(Of T)).
      Um jedoch die getroffene Auswahl ins Viewmodel zu kommunizieren, muß man üblicherweise zusätzlich eine SelectedIndex-Property auch im Viewmodel anlegen und Listbox.SelectedIndex daran binden.

      XML-Quellcode

      1. <Window x:Class="MainWindow2"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:hlp="clr-namespace:System.Windows.Controls;assembly=HelpersSmallEd"
      5. Title="MainWindow2" Height="350" Width="525"
      6. xmlns:my="clr-namespace:PictureViewer"
      7. DataContext="{StaticResource MainModel2}" FontSize="14">
      8. <hlp:GridEx>
      9. <hlp:GridEx.RowDefinitions>
      10. <RowDefinition Height="Auto" MinHeight="10" />
      11. <RowDefinition MinHeight="10" />
      12. </hlp:GridEx.RowDefinitions>
      13. <hlp:GridEx.ColumnDefinitions>
      14. <ColumnDefinition MinWidth="10" Width="Auto" />
      15. <ColumnDefinition MinWidth="10" />
      16. </hlp:GridEx.ColumnDefinitions>
      17. <Menu FontSize="14">
      18. <MenuItem Header="Re_Load" Command="{Binding Path=Reload}" />
      19. <MenuItem Header="_Remove" Command="{Binding Path=DeleteCurrent}" />
      20. </Menu>
      21. <ListBox x:Name="lstFiles" hlp:GridEx.Range="a2" DisplayMemberPath="Name"
      22. ItemsSource="{Binding Path=Files}" SelectedIndex="{Binding Path=SelectedIndex}" />
      23. <StackPanel hlp:GridEx.Range="b1" Orientation="Horizontal" >
      24. <TextBlock Text="{Binding ElementName=lstFiles, Path=SelectedItem.Name}" Margin="6,0" />
      25. <TextBlock Text="{Binding ElementName=lstFiles, Path=SelectedItem.Length, StringFormat=(\{0\} Bytes)}"/>
      26. <TextBlock Text="{Binding Path=PixelSizeText}" Margin="6,0" />
      27. </StackPanel>
      28. <Image hlp:GridEx.Range="b2" Source="{Binding Path=CurrentImage}" />
      29. </hlp:GridEx>
      30. </Window>
      Zu sehen? Die Listbox ist zweifach gebunden: einmal die ItemsSource, und dann der SelectedIndex.

      An Listbox.SelectedItem sind wiederum sind 2 Textblöcke gebunden, die Datei-Informationen anzeigen: .Name und .Length. Hierbei handelt es sich um ein ElementBinding, was auch vom Xaml-Designer recht schön unterstützt wird.
      Die 3. Textbox ist an MainModel2.PixelSizeText gebunden, denn das in der Listbox angewählte FileInfo kann zwar viele Angaben zur Datei machen, aber nicht die Größe des Bildes in Pixeln.
      Ebensowenig kann Image.Source an Listbox.SelectedItem gebunden werden, denn ein FileInfo ist keine ImageSource. Da muß schon das Viewmodel herkommen, und auf Abruf hin das File in ein BitmapImage laden (falls das vom Dateityp her möglich ist).
      Und nur falls das möglich ist, kann auch ein PixelSizeText bereitgestellt werden, ansonsten sei er Nothing.

      Also das Viewmodel:

      VB.NET-Quellcode

      1. Imports System.IO
      2. Imports System.Windows.Media.Imaging
      3. Imports System.Collections.ObjectModel
      4. Imports Microsoft.VisualBasic
      5. Public Class MainModel2 : Inherits MainModelBase(Of MainModel2)
      6. Private _DataDir As New DirectoryInfo("..\..\Data")
      7. Public Property Files As New ObservableCollection(Of FileInfo) From {New FileInfo("c:\Dummi.vb")}
      8. Public Property Reload As New RelayCommand(Sub()
      9. Files.Clear()
      10. _DataDir.GetFiles().ForEach(AddressOf Files.Add)
      11. SelectedIndex = If(Files.Count > 0, 0, -1)
      12. End Sub)
      13. ''' <summary>only deletes from viewed collection - not from FileSystem</summary>
      14. Public Property DeleteCurrent As New RelayCommand( _
      15. Sub() Files.RemoveAt(_SelectedIndex), _
      16. Function() _SelectedIndex >= 0)
      17. Public ReadOnly Property PixelSizeText() As String
      18. Get
      19. With _CurrentImage
      20. Return If(.Null, "<kein Bild>", String.Format("{{ {0} * {1} Pixel }}", .PixelWidth, .PixelHeight))
      21. End With
      22. End Get
      23. End Property
      24. Private _CurrentImage As BitmapImage = Nothing
      25. Public Property CurrentImage() As BitmapImage
      26. Get
      27. Return _CurrentImage
      28. End Get
      29. Private Set(ByVal value As BitmapImage)
      30. If ChangePropIfDifferent(value, "CurrentImage", _CurrentImage) Then RaisePropChanged("PixelSizeText")
      31. End Set
      32. End Property
      33. Private _SelectedIndex As Integer = -1
      34. Public Property SelectedIndex() As Integer
      35. Get
      36. Return _SelectedIndex
      37. End Get
      38. Set(ByVal value As Integer)
      39. If _SelectedIndex = value Then Return
      40. _SelectedIndex = value
      41. If _SelectedIndex < 0 Then
      42. CurrentImage = Nothing
      43. Else
      44. Try
      45. CurrentImage = New BitmapImage(New Uri(Files(_SelectedIndex).FullName))
      46. Catch ex As Exception
      47. CurrentImage = Nothing
      48. End Try
      49. End If
      50. RaisePropChanged("SelectedIndex")
      51. End Set
      52. End Property
      53. End Class
      Es sind 6 Public Properties angeboten, an die zu binden ist:
      1. Public Property Files - eine ObservableCollection, die zunächst mit einem nicht existenten FileInfo gefüttert wird - solche Voreinstellungen sind angenehm im Xaml-Designer

      2. Public Property Reload - ein Command zum Befüllen von Files

      3. Public Property DeleteCurrent - ein Command zum Löschen des aktuell gewählten Files

      4. Public ReadOnly Property PixelSizeText() As String - gibt die BildAbmaße als String formatiert aus

      5. Public Property CurrentImage() As BitmapImage - die Bitmap wird immer neu eingelesen, wenn SelectedIndex sich ändert. Keinesfalls sollen alle Bilder von vornherein eingelesen werden - das würde bei großen Ordnern in eine lang andauernde Operation ausarten, und enorm Speicher verbrauchen.

      6. Public Property SelectedIndex() As Integer - im Setter dieser Property wird das CurrentImage gleich mit-gesetzt (zeilen #50, #53, #55).
      Dateien
      • PictureViewer.zip

        (305,46 kB, 332 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 7 mal editiert, zuletzt von „ErfinderDesRades“ ()

      An CollectionViewSource binden

      Leider folgt obige Funktionalität noch nicht ganz dem, wie man sich Auswahl-Funktionalität vorstellt: Beim DeleteCurrent-Command wird nämlich zwar das aktuelle FileInfo aus der Anzeige entfernt - aber es wird kein anderes Item stattdessen angewählt!
      Bei einer üblichen Auswahl wird nach dem Löschen eines Items immer das nächste angewählt, und falls da keins ist, dann das vorherige. Also wollte man diesen Bug korrigieren, müssteman das DeleteCurrent-Command um ein paar Zeilen erweitern.

      Man könnte auch eine CollectionViewSource in den Resourcen anlegen, und databinding-mäßig zwischenschalten zwischen der MainViewmodel2.Files-Property und den daran zu bindenden Controls.
      Sone CollectionViewSource hat eine Current-Verwaltung, die automatisch ein alternatives Item anwählt, falls das aktuelle gelöscht wird:

      XML-Quellcode

      1. <Window x:Class="MainWindow3"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:hlp="clr-namespace:System.Windows.Controls;assembly=HelpersSmallEd"
      5. Title="MainWindow3" Height="350" Width="525"
      6. xmlns:my="clr-namespace:PictureViewer"
      7. DataContext="{StaticResource MainModel2}" FontSize="14">
      8. <Window.Resources>
      9. <CollectionViewSource x:Key="Files" Source="{Binding Path=Files}" />
      10. </Window.Resources>
      11. <hlp:GridEx>
      12. <hlp:GridEx.RowDefinitions>
      13. <RowDefinition Height="Auto" MinHeight="10" />
      14. <RowDefinition MinHeight="10" />
      15. </hlp:GridEx.RowDefinitions>
      16. <hlp:GridEx.ColumnDefinitions>
      17. <ColumnDefinition MinWidth="10" Width="Auto" />
      18. <ColumnDefinition MinWidth="10" />
      19. </hlp:GridEx.ColumnDefinitions>
      20. <Menu FontSize="14">
      21. <MenuItem Header="Re_Load" Command="{Binding Path=Reload}" />
      22. <MenuItem Header="_Remove" Command="{Binding Path=DeleteCurrent}" />
      23. </Menu>
      24. <ListBox hlp:GridEx.Range="a2" DisplayMemberPath="Name"
      25. ItemsSource="{Binding Source={StaticResource Files}}"
      26. SelectedIndex="{Binding Path=SelectedIndex}"
      27. IsSynchronizedWithCurrentItem="True" />
      28. <StackPanel hlp:GridEx.Range="b1" Orientation="Horizontal" >
      29. <TextBlock Text="{Binding Source={StaticResource Files}, Path=Name}" Margin="6,0" />
      30. <TextBlock Text="{Binding Source={StaticResource Files}, Path=Length, StringFormat=(\{0\} Bytes)}"/>
      31. <TextBlock Text="{Binding Path=PixelSizeText}" Margin="6,0" />
      32. </StackPanel>
      33. <Image hlp:GridEx.Range="b2" Source="{Binding Path=CurrentImage}" />
      34. </hlp:GridEx>
      35. </Window>
      Hier hängen Listbox und die beiden Textblöcke nicht direkt an MainViewmodel2.Files, sondern man hängt an der StaticResource Files, welches eine CollectionViewSource ist.
      Sone CollectionViewSource ist ganz fabelhaft, damit kann man auch filtern, gruppieren und sortieren.
      Aber leider geht das nicht über Bindings ins Viewmodel hinein, sondern allenfalls über CollectionViewSource-Events, die im Codebehind (in "MainWindow3.Xaml.vb") empfangen werden können. Letzteres ist dann ziemlich ungeschickt, denn nach dem MVVM-Pattern sieht man zu, dass User-Eingaben wie die Angabe eines Filterkriteriums im Viewmodel landen, nicht im Codebehind. Und wie teilt man nun der CollectionViewSource ihr Filterkriterium mit?
      Mit Bindings geht das nicht, denn CollectionViewSource ist kein DependancyObject :(

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Die CollectionView ins ViewModel holen

      Also ich fand das unpraktisch, CollectionViewSources im Xaml zu verwenden. Ausserdem verhält sich das immer noch nicht ganz richtig - jetzt im Xaml-Designer: Da wird das Default-Item nicht angezeigt, wassich im MainModel adde. Jdfs dachte ich mir: "Wenn ich meine Current-Verwaltung im Viewmodel brauche, dann kann ich doch einfach dort die CollectionView instanzieren, und im Xaml binde ich dann nicht an die in den Resourcen angelegte CollectionViewSource, sondern an die im Viewmodel instanzierte."
      Dadurch erhalte ich im Viewmodel auch direkte Kontrolle über SelectedIndex und SelectedItem (bei CollectionViewSources heißt es "CurrentPosition/CurrentItem"):D
      Leider ist CollectionView auf Xaml-Instanzierung ausgelegt, und bietet die CurrentItem-Property nur untypisiert As Object an, und ein paar annere Unpässlichkeiten.
      Aber habe ich einfach CollectionView beerbt, und die fehlende Property Current As T dranprogrammiert, und paar annere Kleinigkeiten, und dann siehts ein bischen schnuckeliger aus, sowohl im Viewmodel als auch im Xaml:

      XML-Quellcode

      1. <Window x:Class="MainWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:hlp="clr-namespace:System.Windows.Controls;assembly=HelpersSmallEd"
      5. Title="MainWindow" Height="350" Width="525"
      6. xmlns:my="clr-namespace:PictureViewer"
      7. DataContext="{StaticResource MainModel}" FontSize="14">
      8. <hlp:GridEx>
      9. <hlp:GridEx.RowDefinitions>
      10. <RowDefinition Height="Auto" MinHeight="10" />
      11. <RowDefinition MinHeight="10" />
      12. </hlp:GridEx.RowDefinitions>
      13. <hlp:GridEx.ColumnDefinitions>
      14. <ColumnDefinition MinWidth="10" Width="Auto" />
      15. <ColumnDefinition MinWidth="10" />
      16. </hlp:GridEx.ColumnDefinitions>
      17. <Menu FontSize="14">
      18. <MenuItem Header="Re_Load" Command="{Binding Path=Reload}" />
      19. <MenuItem Header="_Remove" Command="{Binding Path=DeleteCurrent}" />
      20. </Menu>
      21. <ListBox ItemsSource="{Binding Path=Files}" hlp:GridEx.Range="a2" DisplayMemberPath="Name" />
      22. <StackPanel hlp:GridEx.Range="b1" Orientation="Horizontal" >
      23. <TextBlock Text="{Binding Path=Files/Name}" Margin="6,0" />
      24. <TextBlock Text="{Binding Path=Files/Length, StringFormat=(\{0\} Bytes)}"/>
      25. <TextBlock Text="{Binding Path=PixelSizeText}" Margin="6,0" />
      26. </StackPanel>
      27. <Image hlp:GridEx.Range="b2" Source="{Binding Path=CurrentImage}" />
      28. </hlp:GridEx>
      29. </Window>
      Also ich finde das Xaml so viel mehr straight forward: Die Listbox hat als ItemsSource die Files-Auflistung aus dem ViewModel, und weiter brauchts zur Current-Verwaltung nix. Auch die Textboxen sind an dieselbe Files-Auflistung gebunden an .Name- und .Length- - Property des aktuell angewählten FileInfos, bzw. die 3. Textbox ja an den PixelSizeText.

      Das Viewmodel ist auch bischen aufgeräumter: Die Commands können vereinfacht werden, die SelectedIndex-Property ist ja ganz in das ChooseView-Dingens umverlagert, welches jetzt die Current-Verwaltung einheitlich übernimmt, sowohl fürs Xaml als auch fürs Viewmodel.

      VB.NET-Quellcode

      1. Imports System.IO
      2. Imports System.Windows.Media.Imaging
      3. Imports System.Collections.ObjectModel
      4. Public Class MainModel : Inherits MainModelBase(Of MainModel)
      5. Private _DataDir As New DirectoryInfo("..\..\Data")
      6. Public WithEvents Files As New ChooseView(Of FileInfo) From {New FileInfo("c:\Dummi.vb")}
      7. Public Property Reload As New RelayCommand(Sub() Files.ReFillBy(_DataDir.GetFiles, Function(fi) fi.fullname))
      8. ''' <summary>only deletes from viewed collection - not from FileSystem</summary>
      9. Public Property DeleteCurrent As New RelayCommand( _
      10. Sub() Files.RemoveAt(Files.CurrentPosition), _
      11. Function() Files.CurrentPosition >= 0)
      12. Public ReadOnly Property PixelSizeText() As String
      13. Get
      14. With _CurrentImage
      15. Return If(.Null, "<kein Bild>", String.Format("{{ {0} * {1} Pixel }}", .PixelWidth, .PixelHeight))
      16. End With
      17. End Get
      18. End Property
      19. Private _CurrentImage As BitmapImage = Nothing
      20. Public Property CurrentImage() As BitmapImage
      21. Get
      22. Return _CurrentImage
      23. End Get
      24. Private Set(ByVal value As BitmapImage)
      25. If ChangePropIfDifferent(value, "CurrentImage", _CurrentImage) Then RaisePropChanged("PixelSizeText")
      26. End Set
      27. End Property
      28. Private Sub Files_CurrentChanged(ByVal sender As Object, ByVal e As EventArgs) Handles Files.CurrentChanged
      29. CurrentImage = ImageFromFileInfo(Files.Current)
      30. End Sub
      31. ''' <summary>returns Nothing when exception occurs</summary>
      32. Private Shared Function ImageFromFileInfo(ByVal finf As FileInfo) As BitmapImage
      33. With finf
      34. If .Null OrElse Not .Exists Then Return Nothing
      35. Try
      36. Return New BitmapImage(New Uri(.FullName))
      37. Catch ex As Exception
      38. Return Nothing
      39. End Try
      40. End With
      41. End Function
      42. End Class

      Vorsicht mit Copy & Paste
      Der gezeigte Code verwendet allerlei Funktionen und Klassen, die im Framework nicht enthalten, sondern Bestandteil des beiliegenden Helpers-Projektes sind. Also die Samples sind lauffähig, aber in eurer Anwendung wird derselbe Code nur laufen, wenn ihr dort ebenfalls das Helpers-Projekt einbindet, bzw. den dort enthaltenen Code in euer Projekt kopiert oder sonstwie verfügbar macht.

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()