2016-11-28 157 views
6

我創建了一個實現INotifyPropertyChanged接口的基類。此類還包含一個通用函數SetProperty,用於設置任何屬性的值,並在必要時引發PropertyChanged事件。屬性與變量作爲ByRef參數

Public Class BaseClass 
    Implements INotifyPropertyChanged 

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged 

    Protected Function SetProperty(Of T)(ByRef storage As T, value As T, <CallerMemberName> Optional ByVal propertyName As String = Nothing) As Boolean 
     If Object.Equals(storage, value) Then 
      Return False 
     End If 

     storage = value 
     Me.OnPropertyChanged(propertyName) 
     Return True 
    End Function 

    Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional ByVal propertyName As String = Nothing) 
     If String.IsNullOrEmpty(propertyName) Then 
      Throw New ArgumentNullException(NameOf(propertyName)) 
     End If 

     RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName)) 
    End Sub 

End Class 

然後我有一個類,它應該保存一些數據。爲了簡單起見,它只包含一個屬性(在本例中)。

Public Class Item 
    Public Property Text As String 
End Class 

然後,我有第三個類從基類繼承,並使用數據保存類。這第三類應該是WPF窗口的ViewModel。

我沒有列出RelayCommand類的代碼,因爲您可能都有自己的實現。請記住,這個類在執行命令時執行給定的函數。

Public Class ViewModel 
    Inherits BaseClass 

    Private _text1 As Item 'data holding class 
    Private _text2 As String 'simple variable 
    Private _testCommand As ICommand = New RelayCommand(AddressOf Me.Test) 

    Public Sub New() 
     _text1 = New Item 
    End Sub 

    Public Property Text1 As String 
     Get 
      Return _text1.Text 
     End Get 
     Set(ByVal value As String) 
      Me.SetProperty(Of String)(_text1.Text, value) 
     End Set 
    End Property 

    Public Property Text2 As String 
     Get 
      Return _text2 
     End Get 
     Set(ByVal value As String) 
      Me.SetProperty(Of String)(_text2, value) 
     End Set 
    End Property 

    Public ReadOnly Property TestCommand As ICommand 
     Get 
      Return _testCommand 
     End Get 
    End Property 

    Private Sub Test() 
     Me.Text1 = "Text1" 
     Me.Text2 = "Text2" 
    End Sub 

End Class 

然後,我有一個使用視圖模型類作爲其DataContext我的WPF窗口。

<Window x:Class="MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:local="clr-namespace:WpfTest" 
     mc:Ignorable="d" 
     Title="MainWindow" Height="350" Width="525"> 
    <Window.DataContext> 
     <local:ViewModel /> 
    </Window.DataContext> 

    <StackPanel Orientation="Horizontal"> 
     <TextBox Text="{Binding Text1}" Height="24" Width="100" /> 
     <TextBox Text="{Binding Text2}" Height="24" Width="100" /> 
     <Button Height="24" Content="Fill" Command="{Binding TestCommand}" /> 
    </StackPanel> 
</Window> 

正如你所看到的,這個窗口只包含兩個文本框和一個按鈕。文本框綁定到屬性Text1Text2,並且該按鈕應該執行命令TestCommand

當命令執行時,Text1Text2都被賦予一個值。而且由於這兩個屬性都會引發PropertyChanged事件,所以這些值應顯示在我的窗口中。

但我的窗口中只顯示「Text2」的值。

屬性Text1的值爲「Text1」,但似乎此屬性的PropertyChanged事件在屬性獲取其值之前引發。

有沒有辦法改變我的基類中的SetProperty函數,在屬性獲得它的值後提升PropertyChanged

謝謝你的幫助。

+1

http://stackoverflow.com/a/4520101/17034 –

回答

4

究竟發生了什麼?

這不起作用,因爲屬性的行爲不如字段。

當你做Me.SetProperty(Of String)(_text2, value),什麼情況是,參考外地_text2傳遞,而不是它的價值,所以SetProperty功能可以修改有什麼參考內部,字段被修改。

但是,當你做Me.SetProperty(Of String)(_text1.Text, value),編譯器看到的屬性爲getter,所以它會首先調用獲取的_text1財產,然後通過參考返回值作爲參數。因此,當您的功能SetProperty正在接收參數ByRef時,它是來自吸氣器的返回值而不是實際的現場值

從我理解的here,如果你說你的屬性是ByRef,編譯器會在退出函數調用時自動更改字段ref ......所以這可以解釋爲什麼它會在事件後發生變化......

This other blog似乎證實了這種奇怪的行爲。

+0

但是,屬性的​​值會發生變化,但只有在函數SetProperty保留時纔會更改。 – Nostromo

+0

很好找到那篇文章。官方與否,它描述了我看到的行爲,如果我給每個'Item.Text'明確的getter/setter和'Trace.WriteLine()'。 –

3

在C#中,等效代碼不會編譯。因爲像Eric Lippert這樣的人已經進入其他地方的原因,.NET並不舒服地傳遞屬性。(我不明白埃裏克在SO上的某處對C#進行處理,但現在找不到它 - 大體上說,它需要一個奇怪的解決方法或另一個,所有這些都存在C#團隊認爲不可接受的缺點)。

VB會這樣做,但作爲一個相當奇怪的特殊情況:我看到的行爲是我期望的,如果它創建一個臨時變量通過引用傳遞,然後將其值分配給該屬性後該方法完成。這是一個解決方法(由Eric Lippert自己在下面的評論中證實,另請參閱@Martin Verjans的出色答案),這些副作用對於任何不知道如何在.NET中實現byref/ref的人都是違反直覺的。

當你想到它時,他們不能使它正常工作,因爲VB.NET和C#(以及F#和IronPython等等))必須相互兼容,因此VB ByRef參數必須與從C#代碼傳入的C#ref參數兼容。因此,任何解決方法都必須完全是來電者的責任。在理智的範圍內,這限制了它在通話開始之前和通話結束之後所能做的事情。

下面介紹一下ECMA 335 (Common Language Infrastructure) standard不得不說(按Ctrl + F搜索 「byref」):

  • § I.8.2.1.1  管理的指針和相關類型

    一(§I.12.1.1.2)或byref(§I.8.6.1.3,§I.12.4.1.5.2),可以指向一個局部變量,參數,複合類型的字段或數組的元素。 ...

換句話說,只要編譯器而言,ByRef storage As T實際上是在內存中的存儲位置,其中的代碼把一個值的地址。它在運行時非常高效,但不會爲getter和setter提供語法糖魔法。一個屬性一對方法,一個吸氣和一個setter(當然只是一個或另一個)。

因此,如您所述,storage獲得SetProperty()內的新值,並在SetProperty()完成後,_text1.Text具有新的值。但編譯器已經引入了一些神祕的惡魔,它們導致事件的實際順序不符合你的期望。

因此,SetProperty不能用Text1的方式寫出來。我測試過的最簡單的方法是直接在Text1的設置器中調用OnPropertyChanged()

Public Property Text1 As String 
    Get 
     Return _text1.Text 
    End Get 
    Set(ByVal value As String) 
     _text1.Text = value 
     Me.OnPropertyChanged() 
    End Set 
End Property 

有沒有辦法處理這至少有點醜陋。你可以給Text1一個像Text2這樣的常規後臺字段,但是你需要保持與_text1.Text同步。這比上面的IMO更醜陋,因爲你必須保持兩者同步,並且你仍然在Text1二傳手中有額外的代碼。

+0

我不太確定在VB.NET中傳遞屬性是不可能的。我只是測試了給定的場景,並且當作爲參考傳遞時,屬性的值發生了改變。 – Streamline

+0

@Streamline謝謝,我正在寫一些測試代碼。 –

+0

@Streamline你是對的,它確實改變了值 - 但是隻有在'SetProperty'退出之後,完全按照OP的描述。很奇怪。 –