2016-11-28 12 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 

Тогда у меня есть третий класс, который наследуется от базового класса и использует класс хранения данных. Этот третий класс должен быть ViewModel для окна WPF.

Я не перечисляю код для класса 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 

И тогда у меня есть окно WPF, которое использует класс ViewModel как его DataContext.

<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> 

Как видите, это окно содержит только два текстовых блока и кнопку. Текстовые поля привязаны к свойствам Text1 и Text2, и кнопка должна выполнить команду TestCommand.

При выполнении команды свойства Text1 и Text2 получают значение. И поскольку оба свойства повышают событие PropertyChanged, эти значения должны отображаться в моем окне.

Но в моем окне отображается только значение «Текст2».

Стоимость недвижимости 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), компилятор видит поглотитель для свойства, поэтому он будет первым назвать Получить свойство из _text1, а затем передать ссылку на возвращаемое значение в качестве параметра. Поэтому, когда ваша функция SetProperty принимает параметр ByRef, это возвращаемое значение от получателя, , а не фактическое значение поля.

Из того, что я понял here, если вы говорите, что ваша собственность ByRef, компилятор автоматически изменит поле ref при выходе из вызова функции ... Итак, это объяснит, почему оно меняется после вашего события ...

This other blog, похоже, подтверждает это странное поведение.

+0

Но значение свойства изменяется, но только когда функция SetProperty остается. – Nostromo

+0

Хорошо найти эту статью. Официально или нет, он описывает точно поведение, которое я вижу, если я даю явный getter/setter элемента Item.Text с 'Trace.WriteLine()' в каждом. –

3

В C# эквивалентный код не будет компилироваться. .NET не комфортно передает свойства по ссылке, по причинам, по которым такие люди, как Эрик Липперт, ушли в другое место (я смутно вспоминаю, что Эрик обратился к этому вопросу в отношении C# где-то на SO, но не может найти его сейчас - для этого потребуется одно странное обходное решение или другое, все из которых имеют недостатки, которые команда C# считает неприемлемыми).

VB делает это, но как довольно странный частный случай: поведение, которое я вижу, - это то, что я ожидал бы, если бы он создавал временную переменную, которая передается по ссылке, а затем присваивает ее значение свойству после метод завершается. Это обходное решение (подтвержденное самим Эриком Липпертом ниже в комментариях, см. Также замечательный ответ @Martin Verjans) с побочными эффектами, которые противоречат друг другу, кто не знает, как byref/ref реализованы в .NET.

Когда вы думаете об этом, они не могут заставить его работать должным образом, потому что VB.NET и C# (и F #, и IronPython и т. Д. И т. Д.) должен быть взаимно совместимым, поэтому параметр VB ByRef должен быть совместим с аргументом C# ref, переданным из кода C#. Поэтому любое обходное решение должно быть полностью ответственностью вызывающего. В пределах здравого смысла это ограничивает его тем, что он может сделать до начала вызова, и после его возвращения.

Вот что 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 фактически адрес места хранения в памяти, где код помещает значение. Он очень эффективен во время выполнения, но не предлагает возможности для синтаксической сахарной магии с геттерами и сеттерами. Свойством является пара методов, геттер и сеттер (или только один или другой, конечно).

Так как вы описали, storage получает новое значение внутри SetProperty(), и после того, как SetProperty() Завершает, _text1.Text имеет новое значение. Но компилятор представил некоторые оккультные махинации, которые приводят к тому, что фактическая последовательность событий не будет тем, что вы ожидаете.

В результате SetProperty не может использоваться в Text1 способом, которым вы его написали. Простейшим решением, которое я проверил, является вызов OnPropertyChanged() непосредственно в установщике для Text1.

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. Очень странно. –