绑定到ObservableColellection的ItemsControl不会在属性更改时更新UI

mt1985

经过许多头痛和迟到后,我放弃了尝试自己解决问题的方法。尽管有很多文献可以找到非常相似的问题,但我仍无法找到特定问题的确切解决方案。

在修改了ItemsSource中项目的属性后,我无法使用画布作为ItemsPanel来获取ItemsControl,从而无法更新UI。

我创建了一个非常干净的示例应用程序来演示正在发生的事情。

在我的示例应用程序中,我有一个视图“ MainWindow.xaml”,一个视图模型“ MainWindowViewModel.vb”(继承了“ ViewModelBase.vb”)和一个命令委托“ DelegateCommand.vb”,该委托用于创建RelayCommands以更新我的ItemsControl的ItemSource。

首先,MainWindow.xaml:

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SampleApp"
    x:Class="MainWindow" Title="MainWindow" Height="347" Width="525" Background="Black">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <!-- LINE SEGMENTS -->
        <ItemsControl x:Name="ic1" ItemsSource="{Binding LineData, Mode=OneWay, NotifyOnTargetUpdated=True}" HorizontalAlignment="Left" Height="246" VerticalAlignment="Top" Width="517" Background="#FF191919" BorderBrush="#FF444444">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas IsItemsHost="True"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Line X1="{Binding X1}" Y1="{Binding Y1}" X2="{Binding X2}" Y2="{Binding Y2}" Stroke="White" StrokeThickness="6"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

        <Button Content="Refresh Canvas" HorizontalAlignment="Left" Margin="350,261,0,0" VerticalAlignment="Top" Width="124" Height="40" FontFamily="Verdana" FontWeight="Bold" Click="Button_Click"/>
        <Button Content="Command 1" Command="{Binding Command1}" HorizontalAlignment="Left" Margin="45,261,0,0" VerticalAlignment="Top" Width="124" Height="40" FontFamily="Verdana" FontWeight="Bold"/>
        <Button Content="Command 2" Command="{Binding Command2}" HorizontalAlignment="Left" Margin="198,261,0,0" VerticalAlignment="Top" Width="124" Height="40" FontWeight="Bold" FontFamily="Verdana"/>
    </Grid>
</Window>

如您所见,我窗口的DataContext是MainWindowViewModel,而ItemSource的绑定是LineData(位于该VM中)。

另外,我有三个按钮。前两个按钮执行ICommands,而第三个按钮执行ItemsControl的后台代码刷新(这是出于调试目的,以证明在不更新UI的情况下ItemItem内的绑定属性正在更新)。以后再说。

第一个按钮绑定到VM中的Command1,而第二个按钮绑定到VM中的Command2。

接下来,MainWindowViewModel.vb:

Imports System.Collections.ObjectModel

Public Class MainWindowViewModel
    Inherits ViewModelBase

    ' Sample line data variable
    Private _LineData As ObservableCollection(Of LineStructure) = GetLineData()
    Public Property LineData As ObservableCollection(Of LineStructure)
        Get
            Return _LineData
        End Get
        Set(value As ObservableCollection(Of LineStructure))
            _LineData = value
            OnPropertyChanged("LineData")
        End Set
    End Property

    ' ICommands
    Private _Command1 As ICommand
    Public ReadOnly Property Command1 As ICommand
        Get
            If _Command1 Is Nothing Then
                _Command1 = New MVVM.RelayCommand(AddressOf ExecuteCommand1)
            End If
            Return _Command1
        End Get
    End Property

    Private _Command2 As ICommand
    Public ReadOnly Property Command2 As ICommand
        Get
            If _Command2 Is Nothing Then
                _Command2 = New MVVM.RelayCommand(AddressOf ExecuteCommand2)
            End If
            Return _Command2
        End Get
    End Property

    ' ICommand Methods
    Private Sub ExecuteCommand1()
        ' Re-arrange LineData(0) to make a plus sign on the canvas
        ' This works - Assigning a new value to an item of the collection updates the canvas
        LineData(0) = New LineStructure With {.X1 = "175", .Y1 = "50", .X2 = "175", .Y2 = "150"}
    End Sub

    Private Sub ExecuteCommand2()
        ' Put LineData(0) back into its original position
        ' This doesn't work - Modifying the PROPERTY of an item in the collection does not update the canvas.. even with INotifyPropertyChange being called
        LineData(0).X1 = "50"
        LineData(0).Y1 = "50"
        LineData(0).X2 = "300"
        LineData(0).Y2 = "50"

        OnPropertyChanged("LineData")
    End Sub

    ' Misc methods
    Private Function GetLineData() As ObservableCollection(Of LineStructure)
        Dim tmpList As New ObservableCollection(Of LineStructure)

        ' Create two horizontal parallel lines
        tmpList.Add(New LineStructure With {.X1 = "50", .Y1 = "50", .X2 = "300", .Y2 = "50"})
        tmpList.Add(New LineStructure With {.X1 = "50", .Y1 = "100", .X2 = "300", .Y2 = "100"})

        Return tmpList
    End Function
End Class

Public Class LineStructure
    Public Property X1
    Public Property Y1
    Public Property X2
    Public Property Y2
End Class

在我的视图模型中,我立即定义了LineData(这是我的ItemsSource绑定的对象),因此我们有一些ItemSource的数据准备在执行时显示在画布上。它由GetLineData()函数定义,该函数仅返回填充的2行的ObservableCollection。

当应用程序第一次启动时,将显示两条平行的水平线。

LineData变量是我定义的LineStructure类的ObservableObject,它仅包含X1,Y1,X2,Y2字符串,用于将各个对象绑定到画布并显示在画布中。

Command1(再次绑定到第一个按钮)将一个新的LineStructure分配给LineData的第一个索引。当执行此命令时,一切工作正常。用户界面会按预期更新,每个人都很高兴。这使线条在画布上显示为加号。

这是问题开始的地方:

Command2不会像Command1一样为第一个LineData索引分配新的LineStructure,而是要重新定义第一个LineData索引内的属性。如果可行,它将重新排列第一行,并且画布上的两行将再次水平平行。

但是,这不会更新canvas / UI-我不知道为什么。我已经阅读了许多文章,并尝试了许多不同的解决方案,但均无济于事。

如果有人能解释为什么修改属性后绑定不会更新,而不是一起重新声明LineStructure索引,请让我知道,我将不胜感激。

最后要注意的一点是,我设法找到了一种解决方案,可以满足我的需要,但是我不认为我必须使用它。变化。

对于感兴趣的任何人,请参阅以下代码段以获取临时更改解决方案,以在属性更改时更新画布。

我已经将NotifyOnTargetUpdated = True和TargetUpdated =“ RefreshCanvas”添加到了xaml中的ItemsControl声明中。

这就是所谓的RefreshCanvas()方法,该方法从MainWindow的后台代码执行ic1.Items.Refresh()(您可以在本文结尾处找到后台代码)。这将刷新ItemsControl项,从而刷新画布并显示对绑定集合的更新。

<ItemsControl x:Name="ic1" TargetUpdated="RefreshCanvas" ItemsSource="{Binding LineData, Mode=OneWay, UpdateSourceTrigger=PropertyChanged, NotifyOnTargetUpdated=True}" HorizontalAlignment="Left" Height="246" VerticalAlignment="Top" Width="517" Background="#FF191919" BorderBrush="#FF444444">

我将包括其他文件以供参考,因为它可能是相关的:

ViewModelBase.vb:

Imports System.ComponentModel

Public MustInherit Class ViewModelBase
    Implements INotifyPropertyChanged, IDisposable

#Region "Constructor"
    Protected Sub New()
    End Sub
#End Region ' Constructor

#Region "DisplayName"

    ' Returns the user-friendly name of this object.
    ' Child classes can set this property to a new value, or override it to determine the value on-demand.

    Private privateDisplayName As String

    Public Overridable Property DisplayName() As String
        Get
            Return privateDisplayName
        End Get
        Protected Set(ByVal value As String)
            privateDisplayName = value
        End Set
    End Property
#End Region ' DisplayName

#Region "Debugging Aids"
    ' Warns the developer if this object does not have a public property with the specified name. 
    ' This method does not exist in a Release build.
    <Conditional("DEBUG"), DebuggerStepThrough()> _
    Public Sub VerifyPropertyName(ByVal propertyName As String)
        ' Verify that the property name matches a real, public, instance property on this object.
        If TypeDescriptor.GetProperties(Me)(propertyName) Is Nothing Then
            Dim msg As String = "Invalid property name: " & propertyName

            If Me.ThrowOnInvalidPropertyName Then
                Throw New Exception(msg)
            Else
                Debug.Fail(msg)
            End If
        End If
    End Sub

    ' Returns whether an exception is thrown, or if a Debug.Fail() is used when an invalid property name is passed to the VerifyPropertyName method.
    ' The default value is false, but subclasses used by unit tests might override this property's getter to return true.
    Private privateThrowOnInvalidPropertyName As Boolean
    Protected Overridable Property ThrowOnInvalidPropertyName() As Boolean
        Get
            Return privateThrowOnInvalidPropertyName
        End Get
        Set(ByVal value As Boolean)
            privateThrowOnInvalidPropertyName = value
        End Set
    End Property
#End Region ' Debugging Aides

#Region "INotifyPropertyChanged Members"
    ' Raised when a property on this object has a new value.
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    ' Raises this object's PropertyChanged event.
    ' <param name="propertyName">The property that has a new value.</param>
    Protected Overridable Sub OnPropertyChanged(ByVal propertyName As String)
        Me.VerifyPropertyName(propertyName)

        Dim handler As PropertyChangedEventHandler = Me.PropertyChangedEvent
        If handler IsNot Nothing Then
            Dim e = New PropertyChangedEventArgs(propertyName)
            handler(Me, e)
        End If
    End Sub
#End Region ' INotifyPropertyChanged Members

#Region "IDisposable Support"
    Private disposedValue As Boolean ' To detect redundant calls

    ' IDisposable
    Protected Overridable Sub Dispose(disposing As Boolean)
        If Not Me.disposedValue Then
            If disposing Then
                ' TODO: dispose managed state (managed objects).
            End If

            ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
            ' TODO: set large fields to null.
        End If
        Me.disposedValue = True
    End Sub

    ' Invoked when this object is being removed from the application and will be subject to garbage collection.
    Public Sub Dispose() Implements IDisposable.Dispose
        Me.OnDispose()
    End Sub

    ' Child classes can override this method to perform clean-up logic, such as removing event handlers.
    Protected Overridable Sub OnDispose()
    End Sub

    ' Controla el tancament del ViewModel.
    ' <returns></returns>
    ' <remarks></remarks>
    Public Overridable Function CanClose() As Boolean
        Return Nothing
    End Function

#If DEBUG Then
    ' Useful for ensuring that ViewModel objects are properly garbage collected.
    Protected Overrides Sub Finalize()
        Dim msg As String = String.Format("{0} ({1}) ({2}) Finalized", Me.GetType().Name, Me.DisplayName, Me.GetHashCode())
        System.Diagnostics.Debug.WriteLine(msg)
    End Sub
#End If
#End Region

End Class

DelegateCommand.vb:

Imports System.Windows.Input

Namespace MVVM
    Public NotInheritable Class RelayCommand
        Implements ICommand

#Region " Declarations "
        Private ReadOnly _objCanExecuteMethod As Predicate(Of Object) = Nothing
        Private ReadOnly _objExecuteMethod As Action(Of Object) = Nothing
#End Region

#Region " Events "
        Public Custom Event CanExecuteChanged As EventHandler Implements System.Windows.Input.ICommand.CanExecuteChanged
            AddHandler(ByVal value As EventHandler)
                If _objCanExecuteMethod IsNot Nothing Then
                    AddHandler CommandManager.RequerySuggested, value
                End If
            End AddHandler

            RemoveHandler(ByVal value As EventHandler)
                If _objCanExecuteMethod IsNot Nothing Then
                    RemoveHandler CommandManager.RequerySuggested, value
                End If
            End RemoveHandler

            RaiseEvent(ByVal sender As Object, ByVal e As System.EventArgs)
                If _objCanExecuteMethod IsNot Nothing Then
                    CommandManager.InvalidateRequerySuggested()
                End If
            End RaiseEvent
        End Event
#End Region

#Region " Constructor "
        Public Sub New(ByVal objExecuteMethod As Action(Of Object))
            Me.New(objExecuteMethod, Nothing)
        End Sub

        Public Sub New(ByVal objExecuteMethod As Action(Of Object), ByVal objCanExecuteMethod As Predicate(Of Object))
            If objExecuteMethod Is Nothing Then
                Throw New ArgumentNullException("objExecuteMethod", "Delegate comamnds can not be null")
            End If

            _objExecuteMethod = objExecuteMethod
            _objCanExecuteMethod = objCanExecuteMethod
        End Sub
#End Region

#Region " Methods "
        Public Function CanExecute(ByVal parameter As Object) As Boolean Implements System.Windows.Input.ICommand.CanExecute
            If _objCanExecuteMethod Is Nothing Then
                Return True
            Else
                Return _objCanExecuteMethod(parameter)
            End If
        End Function

        Public Sub Execute(ByVal parameter As Object) Implements System.Windows.Input.ICommand.Execute
            If _objExecuteMethod Is Nothing Then
                Return
            Else
                _objExecuteMethod(parameter)
            End If
        End Sub
#End Region
    End Class
End Namespace


Namespace MVVM
    Public NotInheritable Class RelayCommand(Of T)
        Implements ICommand

#Region " Declarations "
        Private ReadOnly _objCanExecuteMethod As Predicate(Of T) = Nothing
        Private ReadOnly _objExecuteMethod As Action(Of T) = Nothing
#End Region

#Region " Events "
        Public Custom Event CanExecuteChanged As EventHandler Implements System.Windows.Input.ICommand.CanExecuteChanged
            AddHandler(ByVal value As EventHandler)
                If _objCanExecuteMethod IsNot Nothing Then
                    AddHandler CommandManager.RequerySuggested, value
                End If
            End AddHandler

            RemoveHandler(ByVal value As EventHandler)
                If _objCanExecuteMethod IsNot Nothing Then
                    RemoveHandler CommandManager.RequerySuggested, value
                End If
            End RemoveHandler

            RaiseEvent(ByVal sender As Object, ByVal e As System.EventArgs)
                If _objCanExecuteMethod IsNot Nothing Then
                    CommandManager.InvalidateRequerySuggested()
                End If
            End RaiseEvent
        End Event
#End Region

#Region " Constructors "
        Public Sub New(ByVal objExecuteMethod As Action(Of T))
            Me.New(objExecuteMethod, Nothing)
        End Sub

        Public Sub New(ByVal objExecuteMethod As Action(Of T), ByVal objCanExecuteMethod As Predicate(Of T))
            If objExecuteMethod Is Nothing Then
                Throw New ArgumentNullException("objExecuteMethod", "Delegate comamnds can not be null")
            End If

            _objExecuteMethod = objExecuteMethod
            _objCanExecuteMethod = objCanExecuteMethod
        End Sub
#End Region

#Region " Methods "
        Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
            If _objCanExecuteMethod Is Nothing Then
                Return True
            Else
                Return _objCanExecuteMethod(DirectCast(parameter, T))
            End If
        End Function

        Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
            _objExecuteMethod(DirectCast(parameter, T))
        End Sub
#End Region
    End Class
End Namespace

MainWindow.xaml.vb(MainWindow的代码背后):

Class MainWindow 
    Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
        ic1.Items.Refresh()
    End Sub

    Private Sub RefreshCanvas(sender As Object, e As DataTransferEventArgs)
        sender.Items.Refresh()
    End Sub
End Class

感谢您提供的任何帮助,以帮助我指出正确的方向,希望这也可以帮助其他人。


*****更新,已解决问题*****


E-Bat如此仁慈地指出,LineData结构本身的属性需要实现INotifyPropertyChanged。我已经实现了此更改,并在下面添加了更新且正常工作的“ MainWindowViewModel.xaml”代码:

Imports System.ComponentModel
Imports System.Collections.ObjectModel

Public Class MainWindowViewModel
    Inherits ViewModelBase

    ' Sample line data variable
    Private _LineData As ObservableCollection(Of LineData) = GetLineData()
    Public Property LineData As ObservableCollection(Of LineData)
        Get
            Return _LineData
        End Get
        Set(value As ObservableCollection(Of LineData))
            _LineData = value
            OnPropertyChanged("LineData")
        End Set
    End Property

    ' ICommands
    Private _Command1 As ICommand
    Public ReadOnly Property Command1 As ICommand
        Get
            If _Command1 Is Nothing Then
                _Command1 = New MVVM.RelayCommand(AddressOf ExecuteCommand1)
            End If
            Return _Command1
        End Get
    End Property

    Private _Command2 As ICommand
    Public ReadOnly Property Command2 As ICommand
        Get
            If _Command2 Is Nothing Then
                _Command2 = New MVVM.RelayCommand(AddressOf ExecuteCommand2)
            End If
            Return _Command2
        End Get
    End Property

    ' ICommand Methods
    Private Sub ExecuteCommand1()
        ' Re-arrange LineData(0) to make a plus sign on the canvas
        ' This works - Assigning a new value to an item of the collection updates the canvas
        LineData(0) = New LineData With {.X1 = "175", .Y1 = "50", .X2 = "175", .Y2 = "150"}
    End Sub

    Private Sub ExecuteCommand2()
        ' Put LineData(0) back into its original position
        ' Now it works, it's voodoo!
        LineData(0).X1 = "50"
        LineData(0).Y1 = "50"
        LineData(0).X2 = "300"
        LineData(0).Y2 = "50"
    End Sub

    ' Misc methods
    Private Function GetLineData() As ObservableCollection(Of LineData)
        Dim tmpList As New ObservableCollection(Of LineData)

        ' Create two horizontal parallel lines
        tmpList.Add(New LineData With {.X1 = "50", .Y1 = "50", .X2 = "300", .Y2 = "50"})
        tmpList.Add(New LineData With {.X1 = "50", .Y1 = "100", .X2 = "300", .Y2 = "100"})

        OnPropertyChanged("LineData")

        Return tmpList
    End Function
End Class

Public Class LineData
    Implements INotifyPropertyChanged

    Private _X1 As String
    Public Property X1 As String
        Get
            Return _X1
        End Get
        Set(value As String)
            _X1 = value
            OnPropertyChanged("X1")
        End Set
    End Property

    Private _Y1 As String
    Public Property Y1 As String
        Get
            Return _Y1
        End Get
        Set(value As String)
            _Y1 = value
            OnPropertyChanged("Y1")
        End Set
    End Property

    Private _X2 As String
    Public Property X2 As String
        Get
            Return _X2
        End Get
        Set(value As String)
            _X2 = value
            OnPropertyChanged("X2")
        End Set
    End Property

    Private _Y2 As String
    Public Property Y2 As String
        Get
            Return _Y2
        End Get
        Set(value As String)
            _Y2 = value
            OnPropertyChanged("Y2")
        End Set
    End Property

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Protected Sub OnPropertyChanged(ByVal name As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
    End Sub
End Class
电子战

当您从ObservableCollection替换项目时,将首先删除旧的引用,然后添加新的引用,因此ObservableCollection将增加其事件,因此第一个命令可以用作魔术。

现在,对于第二条刷新UI的命令,您必须创建INotifyPropertyChanged的实现者LineStructure本身,以便对其属性的任何更改都将通过绑定刷新。因此,告别此类的自动化属性。

Public Class LineStructure
    Implements INotifyPropertyChanged

    Private _x1 As String
    Public Property X1 As String
        Get
            Return _x1
        End Get
        Set(value As String)
            If _x1 = value Then Return
            _x1 = value
            OnPropertyChanged("X1")
        End Set
    End Property
End Class

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

AngularJS指令不会在范围变量更改时更新

UI-Router-范围不会在状态更改时被破坏?

属性更改时,XAML数据绑定不会更新UI

实现ControlValueAccessor的Angular 2指令不会在更改时更新'touched'属性

ReactJS组件textarea不会在状态更改时更新

V绑定不会在商店状态更改时更新类

NSLocale NSLocale preferredLanguages不会在语言更改时更新

ngModel不会在更改时更新值

Bootstrap v4 popover不会在React中的状态更改时更新

React useCallback不会在状态更改时更新

useEffect不会在路由更改时更新状态

React JS组件不会在状态更改时更新

AngularJS不会在异步模型更改时更新视图

单元格样式绑定不会在值更改时更新

角度图不会在数据更改时更新

AngularJS 1.5:scope。$ watch内部链接函数不会在模型更改时更新

属性更改时,UI不会更新

Webpack不会在更改时重建

mapStateToProps 不会在更改时更新组件

当属性更改时,DataGridCheckBoxColumn 不会在 MVVM 中更新

ReactJS 组件不会在父状态更改时更新

Angular 组件属性不会在引用属性更改时更新

Angular - ngOnChanges() 不会在属性更改时执行

React 视图不会在状态更改时更新

绑定控件不会在源更改时更新

Vue 子道具不会在父项中的绑定值更改时更新

useEffect() 不会在属性对象更改时触发(深度对象)

自定义 vue 组件不会在值更改时更新

vue 计算的 ant watch 不会在嵌套属性更改时触发