none
WPF Datenbindung über mehrere TabItems in einem TabControl hinweg RRS feed

  • Frage

  • Hallo,

    ich hatte unter http://social.msdn.microsoft.com/Forums/de-DE/wpfde/thread/a99110ba-70b6-418d-9692-adca583556cf eine Frage bzgl. Datenbindung in WPF gestellt. Die Antwort bzw. das Beispiel von Peter Fleischer war für mich nachvollziehbar und hilfreich: Sofern die View und die DetailView sich in der gleichen xaml-Datei befinden, klappt alles problemlos, d.h. die an DetailView gebundenen Controls aktualisieren sich, sobald ein Datensatz aus dem an die View gebundenen Datagrid ausgewählt wird.

    Nun habe ich den Layout-Aufbau dieses Beispiels wie folgt erweitert: In einem TabControl wird im 1. TabItem das an die View gebundene DataGrid dargestellt. Sobald hier eine Row ausgewählt wird, erstellt eine entsprechende Funktion ein neues TabItem, welches die DetailView darstellt.

    Das Anzeigen und Legen des Focus auf das neue TabItem klappt soweit - Aber egal, welches Item in der View ausgewählt wird, in der DetailView im neuen TabItem wird immer nur der Datensatz mit der ID=1 angezeigt. Anscheinend wird die currentItem-Eigenschaft der View nicht aktualisiert. Woran könnte das liegen? Hier die relevanten Code Snippets:

    Properties der view und und detailView in ViewModel.vb:

     Public ReadOnly Property MoviesView As ICollectionView
    
      Get
    
    
    
       If moviesViewSource Is Nothing Then
    
    
    
        moviesViewSource = New CollectionViewSource
    
        moviesViewSource.Source = dc.view_Movies_Tags_Actors.ToList()
    
        AddHandler moviesViewSource.View.CurrentChanged, Sub()
    
                      RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("MovieDetailView"))
    
                     End Sub
    
        moviesViewSource.SortDescriptions.Add(New SortDescription("Name", ListSortDirection.Ascending))
    
       End If
    
       Return moviesViewSource.View
    
      End Get
    
     End Property
    
    
    
     
    
    
    
     Public ReadOnly Property MovieDetailView As Object
    
      Get
    
       Return dc.Movies.Where(Function(p) p.id = CType(MoviesView.CurrentItem, view_Movies_Tags_Actors).id)
    
      
    
      End Get
    
     End Property
    
    

    Das detailview-Model wird wie folgt erzeugt (Ausschnitt aus Klasse TabPanelView):

     

     Private Sub ShowDetailMovie(ByVal sender As System.Object, ByVal e As System.Windows.Input.MouseButtonEventArgs)
    
    
    
      Dim newControl As New MovieDetailView
    
    
    
      Dim ti As New TabItem()
    
      ti.Header = DGGrid.SelectedItem.name.ToString
    
      ti.Content = newControl
    
    
    
      myTabControl.Items.Add(ti)
    
      myTabControl.SelectedItem = ti
    
    
    
     End Sub
    
    

    So funktioniert es leider nicht. Aber sobald ich die Struktur mit dem Tabcontrol über den Haufen werfe und die beiden view- und detailview-UserControls direkt in mainwindow.xaml einbinde (keine Änderungen am viewModel!), funktioniert die Abhängigkeit der Datenbindung wieder. Was mache ich falsch bzw. habe ich nicht berücksichtigt?

     

    Mittwoch, 28. Juli 2010 16:40

Antworten

  • Mein Beispiel geht davon aus, dass es eine Sicht für eine komplexe Bindung gibt (View) und eine Sicht für den aktuellen Datensatz (DetailView). Wenn also in der Sicht für die komplexe Bindung (View) navigiert wird, folgt die Detailsicht (DetailView) immer, indem immer der aktuelle Datensatz bereitgestellt wird. Wenn die gleiche Detailsicht für die Bindung in unterschiedlichen UIElementen genutzt wird, dann laufen die Anzeigen in diesen UIElemnten synchron.
     
    Wenn Du unterschiedliche TabItems dynamisch erzeugen willst und jedes TabItem an einem anderen Datensatz gebunden werden soll, dann kannst Du nicht an die gleiche Detailsicht binden, sondern musst an den jeweiligen Datensatz binden. In der Ereignisroutine, die das zusätzliche TabItem hinzufügt, holst Du Dir auf dem DetailView den aktuellen Datensatz und nutzt diesen als DataContext für das TabItem. Das könnte so aussehen:
     
     

    #Region " Command for opening tabpage"
    
     Public ReadOnly Property cmdOpenTabPage As ICommand
      Get
       Return New RelayCommand(New Action(Of Object)(AddressOf cmdOpenTabPageExec))
      End Get
     End Property
    
     Private Sub cmdOpenTabPageExec(ByVal obj As Object)
      If obj IsNot Nothing Then
       Dim curr = CType(obj, DetailClass)
       Dim tp = CType((From e In tc.Items _
            Where TypeOf e Is TabItem _
            AndAlso TypeOf CType(e, TabItem).DataContext Is DetailClass _
            AndAlso CType(CType(e, TabItem).DataContext, DetailClass).Current Is curr.Current).FirstOrDefault, TabItem)
       If tp Is Nothing Then
        tp = New TabItem With {.DataContext = curr, .Header = "ID " & curr.Current.Row("ID").ToString}
        tc.Items.Add(tp)
        tp.Content = New WpfControlLibrary1.DetailUI
       End If
       tp.IsSelected = True
       OnPropertyChanged(String.Empty)
      End If
     End Sub
    
    #End Region
    

    --
    Viele Gruesse
    Peter

    • Als Antwort markiert AKR715 Freitag, 30. Juli 2010 10:39
    Freitag, 30. Juli 2010 07:59
  • Danke Peter,

    mit dem RelayCommands-Konzept bin ich (noch) nicht vertraut. Und außerdem scheinst du in deinem o.a. Code zu prüfen, ob der jeweilige Detaildatensatz schon als TabItem erzeugt wurde - das muss ich ebenfalls noch realisieren. Aber deine Hinweise waren für mich sehr wertvoll!

    Ich habe es jetzt wie folgt gelöst (MouseDoubleClick auf mein DataGrid mit den Datensätzen löst u.a. Methode aus, die ein neues Tab mit dem entsprechenden Detaildatensatz erzeugt):

    Private Sub addTab(ByVal sender As System.Object, ByVal e As System.Windows.Input.MouseButtonEventArgs)
    
      Dim newControl As New MovieDetailView
    
      Try
       Dim resource As Object = Me.FindResource("vm") 'vm = meine viewModel-Klasse, die ich im XAML über vm referenziere
       newControl.DataContext = resource.MovieDetailView
    
      Catch ex As ResourceReferenceKeyNotFoundException
       MessageBox.Show("Resource not found.")
      End Try
    
      
      Dim ti As New TabItem()
      ti.Header = DGGrid.SelectedItem.name.ToString
      ti.Content = newControl
    
      myTabControl.Items.Add(ti)
      myTabControl.SelectedItem = ti
    
     End Sub
    • Als Antwort markiert AKR715 Freitag, 30. Juli 2010 10:39
    Freitag, 30. Juli 2010 10:39

Alle Antworten

  • Es freut mich, dass Dir mein Codeschnipsel geholfen hat.
     
    Da Du nicht zeigst, wie Du die Datenquelle gebunden hast, vermute ich, dass Du in deinem MovieDetailView eine separate Instanz eines ViewModels erzeugst. Dieses separate Objekt läuft dann ohne zusätzliche Ma�?nahmen nicht mehr synchron mit dem ViewModel-Objekt im 1. TabItem.
     
    Im Sinne der Trennung von UI und Programm finde ich au�?erdem Deine Herangehensweise nicht optimal. Wenn Du auf die Anzahl der TabItems im ViewModel einwirken willst, dann binde die Tab-Collection des TabControls oder bei fester Anzahl der TabItems binde Visible der einzelne TabItems.
     

    --
    Viele Gruesse
    Peter

    Mittwoch, 28. Juli 2010 18:30
  • Hallo Peter,

    du hast Recht, ich hatte in einem der eingebundenen UserControls eine neue Instanz des ViewModels erzeugt. Nachdem ich diese entfernt hatte und mich nun auf die im MainWindow erzeugte Instanz des View Models berufe, funktioniert es.

    Aber: Wenn ich nun nacheinander drei Rows in meinem dataGrid selektiere, werden zwar drei neue MovieDetail-TabItems erzeugt und der Header korrekt gesetzt. Da aber alle drei MovieDetail-Views an dieselbe Property des ViewModels gebunden sind (die ja das aktuell in der View selektierte Item zurückliefert), werden in allen drei Tabs das zuletzt selektierte Item angezeigt. Dies ist so nicht gewünscht - Wenn ich einen neues MovieDetail-TabItem erzeuge, soll dieses an ein bestimmtes Element meiner View gebunden werden, aber nicht an das aktuelle Element. D.h. , ein Binden an die currentItem-Eingenschaft der View ist hier nicht korrekt.

    Wie kann ich dieses lösen? Ich müsste doch eigentlich für jede MovieDetailView eine eigene Property erstellen, die dann jeweils ein spezielles "fixiertes Item aus der Movie-Collection enthält, oder? Wie berwerkstellige ich dies? Bzw. gibt es einen anderen, besseren Ansatz?

    Und wie ist in diesem Kontekt dein Hinweis "Wenn Du auf die Anzahl der TabItems im ViewModel einwirken willst, dann binde die Tab-Collection des TabControls" zu verstehen? Woran soll ich die Tab-Collection binden? Die Anzahl der TabItems ist flexibel.

    Danke für deine Hilfe!

     

    Donnerstag, 29. Juli 2010 09:30
  • Hallo,

    ich strebe nun eine Lösung wie folgt an: Bei select eines Datensatzes in der Masterview soll eine neue DetailView instanziiert und als Content an ein TabItem übergeben, welches dem TabControl hinzugefügt wird (funkioniert soweit). Die Datenbindung möchte ich dynamisch -auf Basis der ID des selektierten Datensatzes in der Masterview-  hinzufügen. Kann ich grundsätzlich eine View auch an eine Funktion (des ViewModels) binden, oder funktioniert die Datenbindung nur über Properties?

    Wie funktioniert das dynamische Erstellen der Bindung? Aktuell sieht mein Code wie folgt aus, aber so funktioniert es nicht:

     Private Sub ShowDetailMovie(ByVal sender As System.Object, ByVal e As System.Windows.Input.MouseButtonEventArgs)
    
        Dim movieDetailView As New MovieDetailView
        Dim binding1 As New Binding("getMovieDetailView(DGGrid.SelectedItem.id)") 'funktioniert so nicht ...
    
        movieDetailView.SetBinding(movieDetailView.DataContextProperty, binding1)
    
        Dim ti As New TabItem()
        ti.Header = DGGrid.SelectedItem.Name.ToString
        ti.Content = movieDetailView
    
        myTabControl.Items.Add(ti)
        myTabControl.SelectedItem = ti
    
     End Sub

    Rückgabe-Funktion der View:

    Public Function getMovieDetailView(ByVal selectedMovieID As Integer)
    
        Return dc.Movies.Where(Function(p) p.id = selectedMovieID)
    
    End Function  
    

     

      

    Donnerstag, 29. Juli 2010 14:23
  • Mein Beispiel geht davon aus, dass es eine Sicht für eine komplexe Bindung gibt (View) und eine Sicht für den aktuellen Datensatz (DetailView). Wenn also in der Sicht für die komplexe Bindung (View) navigiert wird, folgt die Detailsicht (DetailView) immer, indem immer der aktuelle Datensatz bereitgestellt wird. Wenn die gleiche Detailsicht für die Bindung in unterschiedlichen UIElementen genutzt wird, dann laufen die Anzeigen in diesen UIElemnten synchron.
     
    Wenn Du unterschiedliche TabItems dynamisch erzeugen willst und jedes TabItem an einem anderen Datensatz gebunden werden soll, dann kannst Du nicht an die gleiche Detailsicht binden, sondern musst an den jeweiligen Datensatz binden. In der Ereignisroutine, die das zusätzliche TabItem hinzufügt, holst Du Dir auf dem DetailView den aktuellen Datensatz und nutzt diesen als DataContext für das TabItem. Das könnte so aussehen:
     
     

    #Region " Command for opening tabpage"
    
     Public ReadOnly Property cmdOpenTabPage As ICommand
      Get
       Return New RelayCommand(New Action(Of Object)(AddressOf cmdOpenTabPageExec))
      End Get
     End Property
    
     Private Sub cmdOpenTabPageExec(ByVal obj As Object)
      If obj IsNot Nothing Then
       Dim curr = CType(obj, DetailClass)
       Dim tp = CType((From e In tc.Items _
            Where TypeOf e Is TabItem _
            AndAlso TypeOf CType(e, TabItem).DataContext Is DetailClass _
            AndAlso CType(CType(e, TabItem).DataContext, DetailClass).Current Is curr.Current).FirstOrDefault, TabItem)
       If tp Is Nothing Then
        tp = New TabItem With {.DataContext = curr, .Header = "ID " & curr.Current.Row("ID").ToString}
        tc.Items.Add(tp)
        tp.Content = New WpfControlLibrary1.DetailUI
       End If
       tp.IsSelected = True
       OnPropertyChanged(String.Empty)
      End If
     End Sub
    
    #End Region
    

    --
    Viele Gruesse
    Peter

    • Als Antwort markiert AKR715 Freitag, 30. Juli 2010 10:39
    Freitag, 30. Juli 2010 07:59
  • Danke Peter,

    mit dem RelayCommands-Konzept bin ich (noch) nicht vertraut. Und außerdem scheinst du in deinem o.a. Code zu prüfen, ob der jeweilige Detaildatensatz schon als TabItem erzeugt wurde - das muss ich ebenfalls noch realisieren. Aber deine Hinweise waren für mich sehr wertvoll!

    Ich habe es jetzt wie folgt gelöst (MouseDoubleClick auf mein DataGrid mit den Datensätzen löst u.a. Methode aus, die ein neues Tab mit dem entsprechenden Detaildatensatz erzeugt):

    Private Sub addTab(ByVal sender As System.Object, ByVal e As System.Windows.Input.MouseButtonEventArgs)
    
      Dim newControl As New MovieDetailView
    
      Try
       Dim resource As Object = Me.FindResource("vm") 'vm = meine viewModel-Klasse, die ich im XAML über vm referenziere
       newControl.DataContext = resource.MovieDetailView
    
      Catch ex As ResourceReferenceKeyNotFoundException
       MessageBox.Show("Resource not found.")
      End Try
    
      
      Dim ti As New TabItem()
      ti.Header = DGGrid.SelectedItem.name.ToString
      ti.Content = newControl
    
      myTabControl.Items.Add(ti)
      myTabControl.SelectedItem = ti
    
     End Sub
    • Als Antwort markiert AKR715 Freitag, 30. Juli 2010 10:39
    Freitag, 30. Juli 2010 10:39
  •  
    mit dem RelayCommands-Konzept bin ich (noch) nicht vertraut.
     
    RelayCommand ist eine Klasse, die es ermöglicht, eine Command-Eigenschaft im XAML an das ViewModel-Objekt zu binden, ohne Codebehind zum XAML zu benötigen:
     

    Public Class RelayCommand
     Implements ICommand
     Private ReadOnly _execute As Action(Of Object)
     Private ReadOnly _canExecute As Predicate(Of Object)
     Public Sub New(ByVal execute As Action(Of Object))
      Me.New(execute, Nothing)
     End Sub
     Public Sub New(ByVal execute As Action(Of Object), ByVal canExecute As Predicate(Of Object))
      ' э�?о п�?ове�?ка �?�?сского �?екс�?а 
      If execute Is Nothing Then
       Throw New ArgumentNullException("execute")
      End If
      Me._execute = execute
      Me._canExecute = canExecute
     End Sub
     Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
      Return If(Me._canExecute Is Nothing, True, Me._canExecute(parameter))
     End Function
     Public Custom Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
      AddHandler(ByVal value As EventHandler)
       AddHandler CommandManager.RequerySuggested, value
      End AddHandler
      RemoveHandler(ByVal value As EventHandler)
       RemoveHandler CommandManager.RequerySuggested, value
      End RemoveHandler
      RaiseEvent(ByVal sender As System.Object, ByVal e As System.EventArgs)
      End RaiseEvent
     End Event
     Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
      Me._execute(parameter)
     End Sub
    End Class
    

     

    Und au�?erdem scheinst du in deinem o.a. Code zu prüfen, ob der jeweilige Detaildatensatz schon als TabItem erzeugt wurde - das muss ich ebenfalls noch realisieren. Aber deine Hinweise waren für mich sehr wertvoll!
    Genau das mache ich, da es nicht sinnvoll ist, mehrere TabPages zu einem Datenobjekt zu haben.
     
    Ich habe es jetzt wie folgt gelöst (MouseDoubleClick auf mein DataGrid mit den Datensätzen löst u.a. Methode aus, die ein neues Tab mit dem entsprechenden Detaildatensatz erzeugt):
     
    Private Sub addTab(ByVal sender As System.Object, ByVal e As System.Windows.Input.MouseButtonEventArgs)

      Dim newControl As New MovieDetailView

      Try
       Dim resource As Object = Me.FindResource("vm") 'vm = meine viewModel-Klasse, die ich im XAML über vm referenziere
       newControl.DataContext = resource.MovieDetailView

    Hier kann ich nicht erkennen, wie Du gewährleistest, dass jede TabPage auch "sein eigenes" Datenobjekt bearbeitet, ohne von einer Navigation in der Sicht beeinflusst zu werden.

    In meinem Code setze ich den DataContext auf das Datenobjekt, welches im Detail zu bearbeiten ist. Damit die TabPage (bzw. TabItem) auch nach dem Schlie�?en wieder aus der Liste (TabControl.Items) entfernt wird, nutze ich eine separate Klasse, die sowohl Datenobjekt als auch Close-Methode kapselt, damit im Detail-Tab ein Close an das ViewModel ausgelöst werden kann, welches die TabPage-Liste verwaltet.

    --
    Viele Gruesse
    Peter

     
     
     
     

     

    Freitag, 30. Juli 2010 12:33