2017-01-24 4 views
1

Ich habe festgestellt, dass, wenn das Hintergrundfeld einer Eigenschaft den Modifikator WithEvents hat, die Wertzuweisung mangels besserer Wörter "nacheilen" kann. Ich habe das Verhalten in einer einfachen Demo wiedergegeben, so dass der Zweck der WithEvents wird hier nicht ersichtlich sein (und damit wird es nicht konstruktiv sein zu sagen: „nur loswerden es“)Verzögerte Zuweisung zu einem WithEvents-Backing-Feld

Public Class ItemViewModel 
    Public Property Id As Integer 
End Class 

Public Class ViewModel 
    Inherits ViewModelBase 

    Private WithEvents _item As ItemViewModel = New ItemViewModel() With {.Id = 0} 
    Public Property Item As ItemViewModel 
     Get 
      Return _item 
     End Get 
     Set(value As ItemViewModel) 
      SetProperty(_item, value) 
     End Set 
    End Property 
... 

SetProperty Definition :

Protected Function SetProperty(Of T)(ByRef field As T, value As T, <CallerMemberName> Optional name As String = Nothing) As Boolean 
    If (EqualityComparer(Of T).Default.Equals(field, value)) Then 
     Return False 
    End If 
    field = value 
    NotifyPropertyChanged(name) 
    Return True 
End Function 

Wenn ich die Item Eigenschaft aktualisieren ein neues Element mit einem inkrementierten id zu sein, wird die Eigenschaft Getter, sobald das Ereignis ausgelöst wird getroffen, wie erwartet. Der Wert des Hintergrundfelds ist jedoch immer noch der alte Wert! Wenn ich ein weiteres Ereignis PropertyChanged direkt nach dem Aufruf SetProperty hinzufüge, wird das Hintergrundfeld an diesem Punkt den richtigen Wert haben. Natürlich, wenn ich WithEvents nehme, funktioniert es wie erwartet mit nur einem Ereignis.

Dies ist das einzige Mal, dass ich SetProperty so scheitern gesehen habe. Was ist das Problem, das WithEvents verursacht?

UPDATE: Wenn ViewModel implementiert INotifyPropertyChanged direkt, anstatt erben von der Basis und löst PropertyChanged nach dem Festlegen des Werts, funktioniert es.

Antwort

4

Was hier passiert ist, dass WithEvents ist eine Funktion, die das .NET Framework selbst nicht nativ unterstützt. VB.NET implementiert es zusätzlich zu .NET. Das Feature ist da, weil es auch von VB6 bereitgestellt wurde. Die Art, wie das Feature in VB6 implementiert wurde, ist jedoch aufgrund eines grundlegenden Unterschieds in den Ereignismodellen zwischen COM und .NET sehr unterschiedlich.

Ich werde nicht gehen, wie VB6 die Funktion implementiert; das ist nicht wirklich relevant. Wichtig ist, wie Ereignisse mit .NET funktionieren. Grundsätzlich müssen Ereignisse mit .NET explizit angehängt und abgehakt werden. Wenn Ereignisse definiert werden, gibt es viele Parallelen zur Definition von Eigenschaften. Insbesondere gibt es eine Methode, die einem Ereignis einen Handler hinzufügt, und eine Methode, die einen Handler entfernt, ähnlich der Symmetrie zwischen den "set" - und "get" -Methoden einer Eigenschaft.

Der Grund dafür, dass Ereignisse solche Methoden verwenden, besteht darin, die Liste angehängter Handler von externen Anrufern zu verbergen. Wenn Code außerhalb einer Klasse Zugriff auf die vollständige Liste der angehängten Handler hätte, wäre es möglich, dass er sich damit befasst, was eine sehr schlechte Programmierpraxis wäre, was möglicherweise zu einem sehr verwirrenden Verhalten führen würde.

VB.NET macht direkte Aufrufe dieser Ereignis "hinzufügen" und "entfernen" -Methoden über die AddHandler und RemoveHandler Betreiber. In C# wird die gleiche zugrunde liegende Operation mit den Operatoren += und -= ausgedrückt, wobei das linke Argument eine Ereignismemberreferenz ist.

Was WithEvents gibt Ihnen ist syntaktischer Zucker, der die AddHandler und RemoveHandler Anrufe versteckt. Was wichtig ist zu erkennen ist, dass die Anrufe immer noch da sind, sie sind nur implizit.

Also, wenn Sie Code wie folgt schreiben:

Private WithEvents _obj As ClassWithEvents 

Private Sub _obj_GronkulatedEvent() Handles _obj.GronkulatedEvent 
    ... 
End Sub 

..you VB fragen.NET, um sicherzustellen, dass welches Objekt auch immer _obj zugewiesen wird (unter Berücksichtigung, dass Sie diese Objektreferenz jederzeit ändern können), sollte das Ereignis GronkulatedEvent von diesem Sub behandelt werden. Wenn Sie die Referenz ändern, sollte das alte Objekt GronkulatedEvent sofort entfernt werden und das neue Objekt GronkulatedEvent angehängt werden.

VB.NET implementiert dies, indem Sie Ihr Feld in eine Eigenschaft verwandeln. Das Hinzufügen von WithEvents bedeutet, dass das Feld _obj (oder in Ihrem Fall _item) nicht eigentlich ein Feld ist. Ein Geheimnis dahinter liegende Feld erzeugt wird, und dann wird _item eine Eigenschaft, deren Implementierung sieht wie folgt aus:

Private __item As ItemViewModel ' Notice this, the actual field, has two underscores 

Private Property _item As ItemViewModel 
    <CompilerGenerated> 
    Get 
    Return __item 
    End Get 
    <CompilerGenerated, MethodImpl(Synchronized)> 
    Set(value As ItemViewModel) 
    Dim previousValue As ItemViewModel = __item 

    If previousValue IsNot Nothing Then 
     RemoveHandler previousValue.GronkulatedEvent, AddressOf _item_GronkulatedEvent 
    End If 

    __item = value 

    If value IsNot Nothing Then 
     AddHandler value.GronkulatedEvent, AddressOf _item_GronkulatedEvent 
    End If 
    End Set 
End Property 

Also, warum dies die „Verzögerung“ führen, dass Sie sehen? Nun, Sie können eine Eigenschaft "ByRef" nicht übergeben. Um etwas "ByRef" zu übergeben, müssen Sie dessen Speicheradresse kennen, aber eine Eigenschaft verbirgt die Speicheradresse hinter den Methoden "get" und "set". In einer Sprache wie C# würden Sie einfach einen Kompilierungsfehler erhalten: Eine Eigenschaft ist kein L-Wert, daher können Sie keinen Verweis darauf übergeben. VB.NET ist jedoch fehlerverzeihender und schreibt zusätzlichen Code hinter die Kulissen, damit die Dinge für Sie funktionieren.

in Ihrem Code Sie übergeben, was wie ein Feld aussehen, das _item Mitglied in SetProperty, die ByRef die Parameter nimmt, so kann er einen neuen Wert schreiben. Aber wegen WithEvents ist das _item Mitglied wirklich eine Eigenschaft. Also, was macht VB.NET? Es schafft eine temporäre lokale Variable für den Anruf zu SetProperty, und ordnet sie dann nach dem Aufruf die Eigenschaft zurück:

Public Property Item As ItemViewModel 
    Get 
    Return _item ' This is actually a property returning another property -- two levels of properties wrapping the actual underlying field -- but VB.NET hides this from you 
    End Get 
    Set 
    ' You wrote: SetProperty(_item, value) 
    ' But the actual code emitted by the compiler is: 
    Dim temporaryLocal As ItemViewModel = _item ' Read from the property -- a call to its Get method 

    SetProperty(temporaryLocal, value) ' SetProperty gets the memory address of the local, so when it makes the assignment, it is actually writing to this local variable, not to the underlying property 

    _item = temporaryLocal ' Once SetProperty returns, this extra "glue" code passes the value back off to the property, calling its Set method 
    End Set 
End Property 

Also, weil WithEvents Ihr Feld auf eine Eigenschaft umgewandelt, VB.NET die tatsächlichen aufzuschieben hatte Zuweisung an die Immobilie bis nach dem Anruf an SetProperty zurückkehrt.

Hoffe das macht Sinn! :-)

+1

Danke Jonathan! –

+0

Follow-up: Es gibt ältere Probleme, die die Verwendung von IDisposable unerschwinglich machen. Da wir IDisposable nicht implementieren können und darauf angewiesen werden, dass Dispose() aufgerufen wird, um die verbleibenden Handles zu bereinigen, dachte man, dass die Verwendung von WithEvents dies umgehen und die Bereinigung für uns übernehmen würde. ** 1) ** Gibt WithEvents tatsächlich die Handles frei, wenn das Objekt nicht mehr verwendet wird, so dass es richtig Garbage Collection gesammelt wird? ** 2) ** Gibt es eine Alternative zu IDisposable, die es uns ermöglichen würde, AddHandler und RemoveHandler zu verwenden, ohne Griffe auszugeben, wenn das Objekt nicht mehr benötigt wird? –

+1

Hmm, naja, nein, 'WithEvents' macht überhaupt nichts in Bezug auf die Entsorgung von Objekten, weil es keine Verbindung zur Lebensdauer des Objekts hat. Es gibt nur einen Weg, um sicherzustellen, dass Objekte, die entsorgt werden müssen, tatsächlich entsorgt werden, und zwar die "Dispose" -Methode. Wenn Sie das Objekt während eines bestimmten Codebereichs verwenden, können Sie dafür sorgen, dass dies automatisch mit einer 'Using'-Anweisung geschieht. –