Laden...

Bindings auf Clone

Letzter Beitrag vor einem Monat 7 Posts 181 Views
Bindings auf Clone

Hallo!

Manchmal gibt es doch noch ein Problem mit Bindings ...

Ich habe eine MVVM-Anwendung bei der ich im ViewModel von einer Eigenschaft (Klasseninstanz) eine Kopie (Clone) erstelle. In der View ist diese gebunden.

Eigenschaft (DVD):
        private DVDEintrag _DVD;
        /// <summary>
        /// Aktueller DVD-Eintrag.
        /// </summary>
        public DVDEintrag DVD { get => _DVD; set { _DVD = value; OnChanged(nameof(DVD)); } }
        
Clone Methode der Eigenschaft:
        /// <summary>
        /// Kopiert den aktuellen DVD-Eintrag. (Neue Instanz)
        /// </summary>
        /// <returns>Kopie des akt. DVD-Eintrages. (Neue Instanz)</returns>
        public DVDEintrag Clone()
        {
            DVDEintrag Neu = new();                                                         // Neue Instanz eines DVD-Eintrages
            Neu.Titel = Titel;                                                              // Wert übernehmen
            if (Ablage != null)                                                             // Im Original: Ablage vorhanden?
            {
                Neu.Ablage = new();                                                         // Neue Instanz der Ablage des DVD-Eintrages
                Neu.Ablage.Einheit = Ablage.Einheit;                                        // Wert übernehmen
                Neu.Ablage.LageBez = Ablage.LageBez;                                        // Wert übernehmen
                Neu.Ablage.OrdnungsNr = Ablage.OrdnungsNr;                                  // Wert übernehmen
                Neu.Ablage.Position = Ablage.Position;                                      // Wert übernehmen
                ...
            }
            ...
            return Neu;                                                            // Neue Instanz des aktuellen DVD-Eintrages zurückgeben

        }
<TextBox x:Name="tboONr" Text="{Binding DVD.Ablage.OrdnungsNr, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

Solang die Eigenschaft(en) meiner geklonten Klasse Stamm-Typen sind (string, integer...) ist das Binding nach der Zuweisung zu der geklonten Instanz korrekt. (Die Eigenschaften erhalten ja auch nur geänderte Werte)

public void RefreshDaten(DVDEintrag dVDEintrag ...)
{
    DVD = dVDEintrag.Clone();                                           // Aktuellen DVD-Eintrag übernehmen
    ...
}

Wenn einer Eigenschaft der Clone-Eigenschaft aber eine neue Instanz einer Klasse zugeordnet wird (wie im obigen Beispiel bei der Eigenschaft Ablage) geht nach Zuweisung der geklonten Instanz zur gebundenen Eigenschaft des ViewModels (DVD = dVDEintrag.Clone())das Binding verloren.

Auch OnChanged() reaktiviert das Binding nicht.

public void RefreshDaten(MainWindow view, DVDEintrag dVDEintrag, ...)
{
    _ = MessageBox.Show("tboOrdnungsNummer (Binding-Status vor Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString());   // Ergebnis: Active
    DVD = dVDEintrag.Clone();                            // Aktuellen DVD-Eintrag übernehmen
    OnChanged(nameof(DVD.Ablage));                       // View aktualisieren -> DVD-Ablage
    OnChanged(nameof(DVD.Ablage.OrdnungsNr));            // View aktualisieren -> DVD-Ablage-OrdnungsNr 
    _ = MessageBox.Show("tboOrdnungsNummer (Binding-Status nach Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString()); // Ergebnis: PathError
    ...
}

Einzig und allein ein Target-Update auf das View-Objekt, an das die ViewModel-Eigenschaft gebunden ist, stellt das Binding wieder her.

public void RefreshDaten(MainWindow view, DVDEintrag dVDEintrag, ...)
{
    _ = MessageBox.Show("tboOrdnungsNummer (Binding-Status vor Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString());   // Ergebnis: Active
    DVD = dVDEintrag.Clone();                            // Aktuellen DVD-Eintrag übernehmen
    OnChanged(nameof(DVD.Ablage));                       // View aktualisieren -> DVD-Ablage
    OnChanged(nameof(DVD.Ablage.OrdnungsNr));            // View aktualisieren -> DVD-Ablage-OrdnungsNr 
    _ = MessageBox.Show("tboOrdnungsNummer (Binding-Status nach Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString()); // Ergebnis: PathError

    view.tboONr.GetBindingExpression(TextBox.TextProperty).UpdateTarget();
    _ = MessageBox.Show("tboOrdnungsNummer (Binding-Status nach Target-Update): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString());   // Ergebnis: Active
    ...
} 

Diese Arbeitsweise kann doch nicht MVVM gerecht sein? Wie kann man das Binding trotz Clone erhalten?

Anmerkung:

Da die DVDEintrag-Klasse serialisiert wird, arbeite ich mit partial class, deshalb der expliziete OnChanged(nameof(DVD.Ablage))-Aufruf, zur Aktualisierung der View.

 /// <summary>
 /// Klasse zur Strukturierung eines DVD-Eintrages.
 /// </summary>
 [Serializable]
 public partial class DVDEintrag
 { 
    /// <summary>
    /// Ablage-Ort der DVD
    /// </summary>
    public AblageEintrag Ablage { get; set; }
    ...
}


public partial class DVDEintrag : INotifyPropertyChanged  // Erweiterung der Datenklasse
{
    /// <summary>
    /// DVDEintrag -> PropertyChanged
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    internal void OnChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}

Bindings basieren auf der Referenz, die beim Erzeugen der View existiert. Ein Clone erzeugt eine neue Referenz, die dem Binding nicht bekannt ist - daher funktioniert natürlich auch nichts. Clone gilt bei Bindungen allgemein als Bad Practise.

Willst Du das unbedingt über Clones lösen, musst Du das Binding manuell ersetzen / neu setzen.

Erledigt

Hallo!

Der Fehler lag an dem Bezug des OnChanged. Es muss natürlich das OnChanged des DVDEintrages  (DVD.OnChanged()) aufgerufen werden! (Und nicht das OnChanged des ViewModels!

public void RefreshDaten(MainWindow view, DVDEintrag dVDEintrag, ...)
{
    _ = MessageBox.Show("tboOrdnungsNummer (Binding-Status vor Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString());   // Ergebnis: Active
    DVD = dVDEintrag.Clone();                            // Aktuellen DVD-Eintrag übernehmen
    DVD.OnChanged(nameof(DVD.Ablage));                   // View aktualisieren -> DVD-Ablage
    _ = MessageBox.Show("tboOrdnungsNummer (Binding-Status nach Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString()); // Ergebnis: Active
    ...
}

Sorry!

Das ist trotzdem kein MVVM, was Du da machst - und kein Binding.
Du verwendest nun halt Events und setzt die manuell.

Hallo Abt!

Für meine Begriffe ist die Referenz auf die Eigenschaft DVD (vom Type DVDEintrag) des ViewModels (welche an die View gebunden wird) geblieben.

Es hat sich nur der "Wert" dieser Eigenschaft geändert. Null-Werte erzeugen scheinbar die PathError-Meldung.


Das verstehe ich nicht?

Ich verwende keine Events. Manuell habe ich den Clones nur die Werte des Originals bei der Initalisierung zugeordnet.

Die Aktualisierung der Anzeige hatte nur wegen dem falschen Bezug des OnChanges nicht funktioniert. Jetzt arbeitet das Binding wieder ganz korrekt.

Zitat von perlfred

Hallo Abt!

Für meine Begriffe ist die Referenz auf die Eigenschaft DVD (vom Type DVDEintrag) des ViewModels (welche an die View gebunden wird) geblieben.

Edit: Okay, wenn der DVDEintrag gar nicht das Viewmodel ist, dann ist das was anderes; das war für mich jetzt so nicht ersichtlich / hab ich übersehen.

Sollen Sub-Eigenschaften Deines Datenmodells über Bindings ansprechbar sein, dann musst Du die ebenfalls in die ViewModel-Hierarchie übernehmen. Vermutlich kommt daher zuerst mein Missverständnis, dass Du eben nur nen Teil als Binding eingebunden hast.


Wenn DVDEintrag ein "Business/"Daten"-Modell ist, dann kann es für komplexe Bindings nicht verwenden werden, da es keinerlei Change-Information (über INotifyPropertyChanged ) liefern kann.

Du brauchst dann für jede Eigenschaft von DVDEintrag auch eine bindbare Eigenschaft im ViewModel.

Vereinfacht:

public DVDEintrag DVD
{
  get =>_DVD;
  set
  {
    _DVD = value;
    OnChanged(nameof(DVD));
  }
}

public DVDAblage Ablage
{
  get =>_DVD.Ablage;
  set
  {
    _DVD.Ablage = value;
    OnChanged(nameof(DVD.Ablage));
    OnChanged(nameof(DVD));
  }
}

Hallo Abt!

Danke für deine Hinweise!!!

Dieses manuelle Aktualisieren mache ich ja nur (nicht "automatisch" im Setter der Eigenschaft) weil ich die Klasse serialisieren muss. Der DVD-Eintrag ist für mich so etwas wie ein Datensatz. Deshalb (wie oben angemerkt) die Lösung über die Partial-Klasse.

Und ja, das sind Sub-Eigenschaften die gebunden werden sollen wobei ein OnChange der übergeordneten Eigenschaft (in meinem Fall Ablage) ausreicht hat, um die untergeordneten Eigenschaft(en) mit zu aktualisieren.

Ahaaa: Dein Beispiel (jetzt erst gesehen) ist für mich gut nachvollziehbar!

Vielen Dank!