ItemsControl bound to ObservableColellection not updating UI on property change - wpf

After a lot of headache and late hours, I've given up on trying to solve the problem to this answer myself. While there is a lot of literature for very similar issues that can be found, I haven't been able to find an exact solution to my particular problem.
I am having trouble getting my ItemsControl using a canvas as the ItemsPanel to update the UI after a property of an item within its ItemsSource has been modified.
I have created a very clean sample application to demonstrate exactly what's going on.
In my sample application, I have a view 'MainWindow.xaml', a viewmodel 'MainWindowViewModel.vb' which is inheriting 'ViewModelBase.vb', and lastly a command delegate 'DelegateCommand.vb' which is used to create RelayCommands to update the ItemSource of my ItemsControl.
First, 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>
As you can see, the DataContext of my Window is MainWindowViewModel, and the binding of the ItemSource is LineData (located within that VM).
In addition, I have three buttons. The first two buttons execute ICommands, while the third button executes a behind-code refresh of the ItemsControl (This is for debugging purposes, to prove that a bound property within the ItemSource is being updated while the UI is not). More on that later.
The first button is bound to Command1 in the VM, while the second button is bound to Command2 in the VM.
Next, 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
In my viewmodel, I have defined LineData immediately (this is what my ItemsSource is bound to), so we have some data for our ItemSource ready to be displayed in the canvas upon execution. It is defined by a GetLineData() function, which simply returns a populated ObservableCollection of 2 lines.
When the application first starts, there are two horizontal, parallel lines displayed.
The LineData variable is an ObservableObject of a LineStructure class that I have defined, which simply contains X1, Y1, X2, Y2 strings for the respective objects to bind to and display within the canvas.
Command1 (again, this is bound to the first button) assigns a new LineStructure to the first index of LineData. When this is executed, everything works fantastic; the UI updates as expected and everyone is happy. This makes the lines appear as a plus sign on the canvas.
Here's where the problem begins:
Command2 is not going to assign a new LineStructure to the first LineData index like Command1 does, instead it's going to re-define the properties within the first LineData index individually. If this were to work, it would re-arrange the first line, and both lines on the canvas would be horizontally parallel again.
This however does not update the canvas/UI - and I can't figure out why. I have read numerous articles and tried many different solutions to no avail.
If anyone can explain why the binding does not update upon modifying a property rather than re-declaring the LineStructure index all together, please let me know, I would greatly appreciate it.
One final thing to note, I have managed to find a solution which will get what I need done, however I don't believe I should have to use it.. I would think the bindings should be able to take care of detecting any property changes.
For anyone interested, see the following snippet for a makeshift solution to update the canvas on a property change.
I have added NotifyOnTargetUpdated=True and TargetUpdated="RefreshCanvas" to my ItemsControl declaration in xaml.
What this does is calls a method named RefreshCanvas(), which executes ic1.Items.Refresh() from the MainWindow's code-behind (you can find the code-behind at the end of this post). This refreshes the ItemsControl items, and thus the canvas is refreshed and displays updates to the bound collection.
<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">
I will include my other files just for reference, since it might be relevant:
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 (the code-behind of 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
Thank you for any help that might be offered to point me in the right direction, and hopefully this can help someone else out as well.
***** UPDATE, ISSUE SOLVED *****
E-Bat has so kindly pointed out that the properties of the LineData structure themselves need to implement INotifyPropertyChanged. I have implemented this change and added the updated and working 'MainWindowViewModel.xaml' code below:
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

When you replace an item from an ObservableCollection, the old reference will be removed first and then it adds the new one, so ObservableCollection will be rising its events and that is why the first commands works as magic.
Now for the second command to refresh the UI you have to make the items itself, LineStructure, implementers of INotifyPropertyChanged so any changes to its properties will be refreshed by the binding. So say goodbye to automated properties for this class.
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

Related

Databinding breaks after reloading DBcontext

I have the following ViewModel, bound to a DataGrid in my view using ItemsSource="{Binding Path=ItemsViewSource.View}":
Public Class DataTableViewModel
Inherits ViewModelBase
Private _dataContext As DbContext
Private _items As IList
Private _itemsType As Type
Private _contextType As Type
Private _itemsViewSource As CollectionViewSource
Private _itemsView As ListCollectionView
'Commands
Private _refreshTableCommand As ICommand
Public ReadOnly Property RefreshTableCommand As ICommand
Get
If _refreshTableCommand Is Nothing Then
_refreshTableCommand = New RelayCommand(AddressOf ExecuteRefreshTable, AddressOf CanExecuteRefreshTable)
End If
Return _refreshTableCommand
End Get
End Property
Private Function CanExecuteRefreshTable() As Boolean
'This command can always run.
Return True
End Function
Private Sub ExecuteRefreshTable()
Me.RefreshTable()
End Sub
Public Property Items As IList
Get
Return Me._items
End Get
Set(value As IList)
_items = value
MyBase.OnPropertyChanged("Items")
End Set
End Property
Public Property ItemsViewSource As CollectionViewSource
Get
Return Me._itemsViewSource
End Get
Set(value As CollectionViewSource)
Me._itemsViewSource = value
MyBase.OnPropertyChanged("ItemsViewSource")
End Set
End Property
Public Property ItemsView As ListCollectionView
Get
Return Me._itemsView
End Get
Set(value As ListCollectionView)
Me._itemsView = value
MyBase.OnPropertyChanged("ItemsView")
End Set
End Property
Public ReadOnly Property DataContext As DbContext
Get
Return Me._dataContext
End Get
End Property
Public ReadOnly Overrides Property IsDirty As Boolean
Get
Return Me._dataContext.ChangeTracker.HasChanges
End Get
End Property
Private Sub RefreshTable()
NewDataContext()
End Sub
Private Sub NewDataContext()
If Me._dataContext IsNot Nothing Then
Me._dataContext.Dispose()
End If
Me._dataContext = Activator.CreateInstance(_contextType)
Me._dataContext.Set(_itemsType).Load
Me.Items = _dataContext.Set(_itemsType).Local
End Sub
Public Sub New(displayName As String, contextType As Type, recordType As Type)
MyBase.DisplayName = displayName
Me._itemsType = recordType
Me._contextType = contextType
NewDataContext()
Me._itemsViewSource = New CollectionViewSource
Me.ItemsViewSource.Source = Me.Items
Me.ItemsView = CType(Me.ItemsViewSource.View, ListCollectionView)
Me.ItemsView.Filter = New Predicate(Of Object)(AddressOf FilterByString)
End Sub
End Class
The code functions perfectly until the RefreshTable() method is called at which point the databinding seems to break. Any new data from the database is loaded into the ViewModel and is available in the Items list, however the new data is not shown in the datagrid and any changes made by the user in the DataGrid are not reflected by the DBContext.ChangeTracker.
What am I missing?
Edit:
Upon further testing, the problem seems to be somehow related to the CollectionViewSource; if I bind directly to the Items property, updated records are properly shown in the DataGrid.
Apparently I've been staring at the screen too long.
The CollectionViewSource needs to be rebuilt as well and the DataGrid notified via OnPropertyChanged("ItemsViewSource").
Fix:
Private Sub RefreshTable()
NewDataContext()
Me._itemsViewSource = New CollectionViewSource
Me.ItemsViewSource.Source = Me.Items
Me.ItemsView = CType(Me.ItemsViewSource.View, ListCollectionView)
Me.ItemsView.Filter = New Predicate(Of Object)(AddressOf FilterByString)
'Let WPF know the data changed in the ViewModel
OnPropertyChanged("ItemsViewSource")
End Sub

Wpf DataGridTemplateColumn ComboBox displayed text after selected an item

I have a datagrid that uses a DataGridTemplateColumn and in the CellEditingTemplate a combobox. Everything works okay until a value is selected. I've spent a ton of time trying to figure out how to make this work. Basically a user selects a text item (Display Text) from the combobox, the value needed for the database is stored as the selected value, and the user only ever sees the display text. The way it behaves now is the user only sees the display text when opening the combobox to select an item. But after the item is selected the selected value is shown. Below is my xaml and vb code and screen shots.
Selecting an item from the combobox shows the display text, which is good.
After the user selects an item, instead of the display text being shown, the selected value is; which is what i don't want.
<Window.Resources>
<ObjectDataProvider x:Key="dispProvider" ObjectType="{x:Type local:QCDisposition}"
MethodName="GetDispositions"></ObjectDataProvider>
<ObjectDataProvider x:Key="defectDeptProvider" ObjectType="{x:Type local:QCDefectDept}"
MethodName="GetDepts"></ObjectDataProvider>
</Window.Resources>
<DataGridTemplateColumn x:Name="DispositionCol" Header="Disposition">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock x:Name="txtDisposition" Text="{Binding DispositionID, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox x:Name="cboDisposition" DisplayMemberPath="DefectDesc" SelectionChanged="DispositionChanged" ItemsSource="{Binding Source={StaticResource dispProvider}}" SelectedItem="DispositionID" SelectedValuePath="DispositionID" SelectedValue="DispositionID" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
<DataGridTemplateColumn.CellStyle>
<Style>
<Setter Property="TextBlock.MinWidth" Value="100" />
<Setter Property="TextBlock.TextAlignment" Value="Left" />
</Style>
</DataGridTemplateColumn.CellStyle>
</DataGridTemplateColumn>
Class to get disposition codes:
Public Class QCDisposition
Implements INotifyPropertyChanged
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
'Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs)
If Not PropertyChangedEvent Is Nothing Then
RaiseEvent PropertyChanged(Me, e)
End If
End Sub
Private _dispositionID As Integer
Public Property QCDispID() As Integer
Get
Return _dispositionID
End Get
Set(ByVal value As Integer)
_dispositionID = value
OnPropertyChanged(New PropertyChangedEventArgs("QCDispositionID"))
End Set
End Property
Private _defectCode As String
Public Property DefectDesc() As String
Get
Return _defectCode
End Get
Set(ByVal value As String)
_defectCode = value
OnPropertyChanged(New PropertyChangedEventArgs("DefectCode"))
End Set
End Property
Public Sub New(dispositionID As Integer, defectCode As String)
Me.QCDispID = dispositionID
Me.DefectDesc = defectCode
End Sub
Public Shared Function GetDispositions() As ICollection(Of QCDisposition)
Try
Dim m_Dispositions As ObservableCollection(Of QCDisposition) = New ObservableCollection(Of QCDisposition)()
Dim db1 As New QCDispositionDataContext
Dim disp = From go In db1.QCDispositionCodes
Select go
For Each g In disp
m_Dispositions.Add(New QCDisposition(g.QCDispositionID, g.DefectCode))
Next
Return m_Dispositions
Catch ex As Exception
MessageBox.Show(ex.Message, "Unhandled Error", MessageBoxButton.OK, MessageBoxImage.Error)
Return Nothing
End Try
End Function
End Class
QC class:
Imports System.ComponentModel
Imports System.Collections.ObjectModel
Public Class QCItem
Inherits ObjectBase
Implements INotifyPropertyChanged
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
Public Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs)
If Not PropertyChangedEvent Is Nothing Then
RaiseEvent PropertyChanged(Me, e)
End If
End Sub
Private _salesOrder As Integer
Public Property SalesOrder() As Integer
Get
Return _salesOrder
End Get
Set(value As Integer)
_salesOrder = value
OnPropertyChanged(New PropertyChangedEventArgs("fldSalesOrder"))
End Set
End Property
Private _productRecordNo As Integer
Public Property ProductRecordNumber() As Integer
Get
Return _productRecordNo
End Get
Set(value As Integer)
_productRecordNo = value
OnPropertyChanged(New PropertyChangedEventArgs("fldProductRecordNumber"))
End Set
End Property
Private _taskNumber As Integer
Public Property TaskNumber() As Integer
Get
Return _taskNumber
End Get
Set(value As Integer)
_taskNumber = value
OnPropertyChanged(New PropertyChangedEventArgs("fldTaskNumber"))
End Set
End Property
Private _qcDesc As String
Public Property Description() As String
Get
Return _qcDesc
End Get
Set(value As String)
_qcDesc = value
OnPropertyChanged(New PropertyChangedEventArgs("fldDescription"))
End Set
End Property
Private _enteredBy As String
Public Property EnteredBy() As String
Get
Return _enteredBy
End Get
Set(value As String)
_enteredBy = value
OnPropertyChanged(New PropertyChangedEventArgs("fldEnteredBy"))
End Set
End Property
Private _enteredDate As String
Public Property EnteredDate() As String
Get
Return Convert.ToString(Convert.ToDateTime(_enteredDate).ToShortDateString)
End Get
Set(value As String)
_enteredDate = value
OnPropertyChanged(New PropertyChangedEventArgs("fldEnteredDate"))
End Set
End Property
Private _completedDate As Nullable(Of Date)
Public Property CompletedDate() As Nullable(Of Date)
Get
Return _completedDate
End Get
Set(value As Nullable(Of Date))
_completedDate = value
OnPropertyChanged(New PropertyChangedEventArgs("fldCompleteDate"))
End Set
End Property
Private _dispositionID As Integer
Public Property DispositionID() As Integer
Get
Return _dispositionID
End Get
Set(value As Integer)
_dispositionID = value
OnPropertyChanged(New PropertyChangedEventArgs("QCDispositionID"))
End Set
End Property
Private _deptCodeID As Decimal
Public Property DeptCodeID() As Decimal
Get
Return _deptCodeID
End Get
Set(value As Decimal)
_deptCodeID = value
OnPropertyChanged(New PropertyChangedEventArgs("QCDeptCodeID"))
End Set
End Property
Public Sub New(ByVal salesOrder As Integer, ByVal productRecNo As Integer, ByVal taskNumber As Integer, ByVal desc As String,
ByVal enteredBy As String, enteredDate As Nullable(Of Date), ByVal completedDate As Nullable(Of Date), ByVal dispositionID As Integer, ByVal deptCodeID As Decimal)
Try
Me.SalesOrder = salesOrder
Me.ProductRecordNumber = productRecNo
Me.TaskNumber = taskNumber
Me.Description = desc
Me.EnteredBy = enteredBy
Me.EnteredDate = enteredDate
Me.CompletedDate = completedDate
Me.DispositionID = dispositionID
Me.DeptCodeID = deptCodeID
Catch ex As Exception
MessageBox.Show(ex.Message, "New_Event", MessageBoxButton.OK, MessageBoxImage.Error)
End Try
End Sub
End Class
Public Class QCItems
Implements INotifyPropertyChanged
Private _qcItem As ICollection(Of QCItem)
Public Property QCItem As ICollection(Of QCItem)
Get
Return _qcItem
End Get
Set(value As ICollection(Of QCItem))
_qcItem = value
OnPropertyChanged(New PropertyChangedEventArgs("QCItem"))
End Set
End Property
Public Sub New()
Me.QCItem = New ObservableCollection(Of QCItem)
End Sub
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
Public Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs)
If Not PropertyChangedEvent Is Nothing Then
RaiseEvent PropertyChanged(Me, e)
End If
End Sub
End Class
ANSWER (thanks to bars222):
<TextBlock x:Name="txtDisposition" Text="{Binding DispositionID, Converter={StaticResource qcDispCvrt}, UpdateSourceTrigger=PropertyChanged}" />
Public Class QCDescConverter
Implements IValueConverter
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements IValueConverter.Convert
Try
If value IsNot Nothing AndAlso value.ToString().Length > 0 Then
Dim iVal As Integer = System.Convert.ToInt32(value)
Dim util As New DBUtil
Dim returnVal As String
returnVal = util.GetDefectDesc(iVal)
Return returnVal
End If
Return Nothing
Catch ex As Exception
Return Nothing
MessageBox.Show(ex.Message, "Insert Note Failed", MessageBoxButton.OK, MessageBoxImage.Error)
End Try
End Function
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
Throw New NotImplementedException()
End Function
End Class
You observe DispositionID in the Disposition column, cause you have CellTemplate with txtDisposition that displayed this.
To display DefectDesc you can add ValueConverter to the txtDisposition binding that converts id to DefectDesc.
Or you can add DefectDesc property in QC class for displaying that description. And you should add code in the DispositionID property setter, that updates DefectDesc. And change txtDisposition binding to DefectDesc.

Reverse Sort ObservableCollection?

Experts,
Short Version:
The way ObservableCollection sorts is the most recent at the end, and I need it to be exactly the opposite, for display in a WPF DataGrid. Right now, all is well, but new entries are added to the end, so the user can't see the new rows added.
Less Short Version:
I have a DateTime field on the Entry class if needed to sort by/on, but to be honest, if I could just add them to the top as I get them, I don't even need to sort! I just need:
*Each item added to the collection to be added to the top, and not the default bottom.*
A Little Longer Version:
I simply cannot find a way to introduce new elements to the 'top' of the collection... "Why?" Well, I am displaying rows of data in a WPF form, and I want the most current on the top, sorted by a date field in the object.
If it is the same as IList, then why is this so hard?
Too complicated? Let me simplify:
Really Long Version:
At the very start, there is a class that will make up a row in a WPF DataGrid. The class is called "Entry", and the only 2 properties that matter below are:
Class Entry
[...]
Public Property TsCreated As Nullable(Of DateTime)
Public Property EntryRaw As String
Set(value As String)
If value <> _entryRaw Then
_entryRaw = value
OnPropertychanged("EntryRaw")
End If
End Set
Get
Return _entryRaw
End Get
End Property
Private _entryRaw As String
[...]
End Class
Next is the ObservableCollection of these Entrys...
Public Class SysEvents
Inherits ObservableCollection(**Of Entry**)
Private Shared _list As New SysEvents
Public Sub New()
End Sub
End Class
ObservableCollection has a non-overridable Sub Add(), so I can't write my own Sub Add() and sort right after adding...
So in use, this WPF Window class is like this (again, making it really simple):
Class MainWindow
Private RawEvents As New toag.syslog.SysEvents
Sub New()
grdEntryRaw.ItemsSource = RawEvents ' grid that holds rows
End Sub
Sub AddRow()
Me.RawEvents.Add(sysEvent)
End Sub
End Class
As if it matters, the XAML (can I sort in XAML?):
<DataGrid x:Name="grdEntryRaw" [...]>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding EntryRaw}" Header="Entry Raw" />
</DataGrid.Columns>
</DataGrid>
OK. Nobody Made It This Far Version:
Since I can't intercept the .Add() that the Binding is doing, it seems like I can't ever get in there with some sorting algorithm...
I thought the fight was over when I figured this much out... But now it seems success has been snatched from me on the 1 yard line! Oh, Visual Studio.. .you are a cruel mistress...
TIA!!!
There's nothing special about the ObservableCollection other than it implements INotifyCollectionChanged and INotifyPropertyChanged.
I suggests that you create your own ObservableCollection with the required behavior.
Public Class ObservableStack(Of T)
Implements IEnumerable, ICollection, IList
Implements IEnumerable(Of T), ICollection(Of T), IList(Of T)
Implements INotifyCollectionChanged, INotifyPropertyChanged
Public Sub New()
Me.list = New List(Of T)
End Sub
'...
Public Sub Add(item As T) Implements ICollection(Of T).Add
'TODO: Validate.
Me.list.Insert(0, item) 'Insert at top of the list.
Me.RaisePropertyChanged("Count")
Me.RaisePropertyChanged("Item")
Me.RaiseCollectionChanged(NotifyCollectionChangedAction.Add, item, 0)
End Sub
Private Function _Add(obj As Object) As Integer Implements IList.Add
Me.Add(TryCast(obj, T))
Return 0
End Function
'...
Private ReadOnly list As List(Of T)
End Class
Example
Public Class ObservableStack(Of T)
Implements IEnumerable, ICollection, IList
Implements IEnumerable(Of T), ICollection(Of T), IList(Of T)
Implements INotifyCollectionChanged, INotifyPropertyChanged
Public Sub New()
Me.list = New List(Of T)
End Sub
Public Event CollectionChanged As NotifyCollectionChangedEventHandler Implements INotifyCollectionChanged.CollectionChanged
Protected Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public ReadOnly Property Count() As Integer Implements ICollection.Count, ICollection(Of T).Count
Get
Return Me.list.Count
End Get
End Property
Default Public Property Item(index As Integer) As T Implements IList(Of T).Item
Get
Return Me.list.Item(index)
End Get
Set(value As T)
Me.Replace(index, value)
End Set
End Property
Private ReadOnly Property _IsFixedSize() As Boolean Implements IList.IsFixedSize
Get
Return CType(Me.list, IList).IsFixedSize
End Get
End Property
Private ReadOnly Property _IsReadOnly() As Boolean Implements IList.IsReadOnly, ICollection(Of T).IsReadOnly
Get
Return CType(Me.list, IList).IsReadOnly
End Get
End Property
Private ReadOnly Property _IsSynchronized() As Boolean Implements ICollection.IsSynchronized
Get
Return CType(Me.list, ICollection).IsSynchronized
End Get
End Property
Private Property _Item(index As Integer) As Object Implements IList.Item
Get
Return Me.Item(index)
End Get
Set(value As Object)
Me.Item(index) = DirectCast(value, T)
End Set
End Property
Private ReadOnly Property _SyncRoot() As Object Implements ICollection.SyncRoot
Get
Return CType(Me.list, ICollection).SyncRoot
End Get
End Property
Public Sub Add(item As T) Implements ICollection(Of T).Add
Me.Insert(0, item)
End Sub
Public Sub Clear() Implements IList.Clear, ICollection(Of T).Clear
If (Me.Count > 0) Then
Me.list.Clear()
Me.RaisePropertyChanged("Count")
Me.RaisePropertyChanged("Item")
Me.RaiseCollectionReset()
End If
End Sub
Public Function Contains(item As T) As Boolean Implements ICollection(Of T).Contains
Return Me.list.Contains(item)
End Function
Public Sub CopyTo(array() As T, index As Integer) Implements ICollection(Of T).CopyTo
Me.list.CopyTo(array, index)
End Sub
Public Function GetEnumerator() As IEnumerator(Of T) Implements IEnumerable(Of T).GetEnumerator
Return Me.list.GetEnumerator()
End Function
Public Function IndexOf(item As T) As Integer Implements IList(Of T).IndexOf
Return Me.list.IndexOf(item)
End Function
Public Sub Insert(index As Integer, item As T) Implements IList(Of T).Insert
'TODO: Validate item.
Me.list.Insert(index, item)
Me.RaisePropertyChanged("Count")
Me.RaisePropertyChanged("Item")
Me.RaiseCollectionChanged(NotifyCollectionChangedAction.Add, item, index)
End Sub
Public Sub Move(ByVal oldIndex As Integer, ByVal newIndex As Integer)
Me.MoveItem(oldIndex, newIndex)
End Sub
Protected Overridable Sub MoveItem(ByVal oldIndex As Integer, ByVal newIndex As Integer)
Dim item As T = Me.Item(oldIndex)
Me.list.RemoveAt(oldIndex)
Me.list.Insert(newIndex, item)
Me.RaisePropertyChanged("Item")
Me.RaiseCollectionChanged(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex)
End Sub
Protected Overridable Sub OnCollectionChanged(e As NotifyCollectionChangedEventArgs)
RaiseEvent CollectionChanged(Me, e)
End Sub
Protected Overridable Sub OnPropertyChanged(e As PropertyChangedEventArgs)
RaiseEvent PropertyChanged(Me, e)
End Sub
Private Sub RaiseCollectionChanged(action As NotifyCollectionChangedAction, item As T, index As Integer)
Me.OnCollectionChanged(New NotifyCollectionChangedEventArgs(action, item, index))
End Sub
Private Sub RaiseCollectionChanged(ByVal action As NotifyCollectionChangedAction, ByVal item As Object, ByVal index As Integer, ByVal oldIndex As Integer)
Me.OnCollectionChanged(New NotifyCollectionChangedEventArgs(action, item, index, oldIndex))
End Sub
Private Sub RaiseCollectionChanged(action As NotifyCollectionChangedAction, oldItem As T, newItem As T, index As Integer)
Me.OnCollectionChanged(New NotifyCollectionChangedEventArgs(action, newItem, oldItem, index))
End Sub
Private Sub RaiseCollectionReset()
Me.OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
End Sub
Private Sub RaisePropertyChanged(propertyName As String)
Me.OnPropertyChanged(New PropertyChangedEventArgs(propertyName))
End Sub
Public Function Remove(item As T) As Boolean Implements ICollection(Of T).Remove
Dim index As Integer = Me.IndexOf(item)
If (index <> -1) Then
Me.RemoveAt(index)
Return True
End If
Return False
End Function
Public Sub RemoveAt(index As Integer) Implements IList.RemoveAt, IList(Of T).RemoveAt
Dim item As T = Me.Item(index)
Me.list.RemoveAt(index)
Me.RaisePropertyChanged("Count")
Me.RaisePropertyChanged("Item")
Me.RaiseCollectionChanged(NotifyCollectionChangedAction.Remove, item, index)
End Sub
Public Sub Replace(index As Integer, newItem As T)
'TODO: Validate item.
Dim oldItem As T = Me.Item(index)
Me.list.Item(index) = newItem
Me.RaisePropertyChanged("Item")
Me.RaiseCollectionChanged(NotifyCollectionChangedAction.Replace, oldItem, newItem, index)
End Sub
Private Function _Add(obj As Object) As Integer Implements IList.Add
Me.Add(DirectCast(obj, T))
Return 0
End Function
Private Function _Contains(obj As Object) As Boolean Implements IList.Contains
Return Me.Contains(DirectCast(obj, T))
End Function
Private Sub _CopyTo(array As Array, index As Integer) Implements ICollection.CopyTo
CType(Me.list, ICollection).CopyTo(array, index)
End Sub
Private Function _GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
Return Me.GetEnumerator()
End Function
Private Function _IndexOf(obj As Object) As Integer Implements IList.IndexOf
Return Me.IndexOf(DirectCast(obj, T))
End Function
Private Sub _Insert(index As Integer, obj As Object) Implements IList.Insert
Me.Insert(index, DirectCast(obj, T))
End Sub
Private Sub _Remove(obj As Object) Implements IList.Remove
Me.Remove(DirectCast(obj, T))
End Sub
Private ReadOnly list As List(Of T)
End Class
#Bjørn-Roger Kringsjå certainly pointed me in the right direction.
I stripped things down to the bare minimum.
Instead of making my own collection class, I created the collection inside the WPF Window class:
declaration at the top of the WPF MainWindow class:
Private RawEvents As ObservableCollection(Of Entry)
Then, I had to instantiate it in the mechanics of the class' instantiation, and se tthe ItemsSource for the DataGrid:
RawEvents = New ObservableCollection(Of Entry)
grdEntryRaw.ItemsSource = RawEvents ' source for main window (events)
The only thing left was to put new events into the collection (I get new events from a message queue, but it matters not:
Public Sub PeekQ(ByVal sender As System.Object, ByVal e As System.Messaging.PeekCompletedEventArgs) Handles Q.PeekCompleted
[..]
' send to main display (newest entries on top)
Me.Dispatcher.Invoke(CType(Sub() **Me.RawEvents.Insert(0, someEvent)**, Action))
'
Me.CountryLookupQ.BeginPeek()
End Sub
...and that is it! I didn't even need an additional class to hold the events... I just used the ObservableCollection created inside the WPF window. The XAML is dead simple, and the best part is that there is no sorting algorithm:
[...]
<DockPanel x:Name="EntryRawDockPanel" HorizontalAlignment="Left" LastChildFill="False" Width="517" Margin="0,26,0,41">
<DataGrid x:Name="grdEntryRaw" Grid.Column="1" Margin="0,0,10,43" AutoGenerateColumns="False" HorizontalContentAlignment="Stretch" CanUserAddRows="False" CanUserDeleteRows="True" AlternatingRowBackground="#FFDEFFE4">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding EntryRaw}" Header="Entry Raw" IsReadOnly="False"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
[...]
Honestly, that is the entire solution. The Entry() class is not special in any way.
Hope this helps someone else, and yes, I ave seen more than one way to do this, like sorting in XAML, and even instantiating a class in XAML, but this is the easiest for my way of writing.

WPF Datagrid DataGridComboBoxColumn Autogenerate at runtime

I have a datagrid in my vb.net 4.0 wpf project. I have seen many examples in XAML on how to bind a DataGridComboBoxColumn however I need to do this in code as I have auto-generating columns. I switch the datagridbinding source to multiple data sets.
Inside some of these custom classes are a couple lists. I can get text and checkboxes to auto-generate correctly. When it comes across my array (I have tried many different kinds) I just see a textboxcolumn with the words (Collection) in it.
For instance - One screen I am grabbing system information on WMI calls. One of the calls returns all IP addresses on the server (We can have up to 8 IP addresses) I do not want a column per IP address. I would like to include a list of those into the datagrid so you can drop down and see them.
Any suggestions on if this is possible or if I am doing something wrong?
Thank you
Sample Code
Imports System.Collections.ObjectModel
Class MainWindow
Dim ServerInfoArray As ObservableCollection(Of ServerInfo) = New ObservableCollection(Of ServerInfo)
Private ReadOnly _ipAddresses As ObservableCollection(Of String) = New ObservableCollection(Of String)
Private Sub GetInfo(ByVal list As List(Of String))
For Each server As String In list
Dim tempip As List(Of String) = New List(Of String)
Dim sinfo As ServerInfo = New ServerInfo
tempip.Add("192.129.123.23")
tempip.Add("23.213.223.21")
sinfo.IPArray = tempip
sinfo.Servername = server
ServerInfoArray.Add(sinfo)
Next
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles Button1.Click
Dim serverlist As List(Of String) = New List(Of String)
serverlist.Add("Test")
serverlist.Add("Random")
serverlist.Add("Local")
GetInfo(serverlist)
End Sub
Private Sub Window_Loaded(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
Dim Col_Serial As DataGridTextColumn = New DataGridTextColumn()
Col_Serial.Binding = New Binding("Servername")
Col_Serial.Header = "Servername"
Col_Serial.Width = 40
Dim Col_IPArray = New DataGridComboBoxColumn()
Col_IPArray.Header = "IP Addresses"
Col_IPArray.IsReadOnly = True
Col_IPArray.ItemsSource = Me._ipAddresses
Col_IPArray.SelectedItemBinding = New Binding("IPArray")
DataGrid1.Columns.Add(Col_Serial)
DataGrid1.Columns.Add(Col_IPArray)
DataGrid1.ItemsSource = ServerInfoArray
End Sub
End Class
Class ServerInfo
Dim _Servername As String
Dim _IPArray As List(Of String)
Public Property Servername() As String
Get
Return _Servername
End Get
Set(ByVal value As String)
_Servername = value
End Set
End Property
Public Property IPArray As List(Of String)
Get
Return _IPArray
End Get
Set(ByVal value As List(Of String))
_IPArray = value
End Set
End Property
Public Sub New()
_Servername = Nothing
_IPArray = New List(Of String)
End Sub
End Class
Not sure if I am understanding perfectly but if you can live with AutoGenerateColumns = False then you can manipulate the columns, their properties and bindings from code like below. So you could use whatever logic you need to in order setup the columns from code. I tried to show how you can source the combobox column items from a different object than the items in the DataGrid as well.
This is meant to be a simple example so I just did everything in the code-behind but from an MVVM standpoint, it depends on your preferred approach but possibility is that you could accomplish the same logic from a controller class that spins up your view models, etc...
Hope it helps!
XAML...
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Button x:Name="btnRefreshIPList">Refresh list of IPs</Button>
<DataGrid x:Name="dataGrid1"></DataGrid>
</StackPanel>
</Window>
Code behind...
Imports System.Collections.ObjectModel
Class MainWindow
'The list of IPs for column's ItemSource property
Private ReadOnly _ipAddresses As ObservableCollection(Of String)
'The items for binding to the DataGrid's ItemsSource
Private _items As List(Of MyObjectWithIPAddress)
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
_ipAddresses = New ObservableCollection(Of String)
_items = New List(Of MyObjectWithIPAddress)
End Sub
Private Sub Window_Loaded(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
Me.dataGrid1.AutoGenerateColumns = False
dataGrid1.Columns.Clear()
'Example of text column (Text bound to Name property)
Dim dgTxtCol = New DataGridTextColumn()
dgTxtCol.Header = "Name"
dgTxtCol.Binding = New Binding("Name")
dataGrid1.Columns.Add(dgTxtCol)
'Example of combobox column (SelectedItem bound to IPAddress)
Dim dgCmbCol = New DataGridComboBoxColumn()
dgCmbCol.Header = "IP Address"
dgCmbCol.ItemsSource = Me._ipAddresses
dgCmbCol.SelectedItemBinding = New Binding("IPAddress")
dataGrid1.Columns.Add(dgCmbCol)
'Add items to DataGrid
_items.Add(New MyObjectWithIPAddress("foo1"))
_items.Add(New MyObjectWithIPAddress("foo2"))
Me.dataGrid1.ItemsSource = Me._items
End Sub
''' <summary>
''' To emulate fetching the object that has the IP list
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Private Function GetIpList() As MyObjectWithListOfIPs
Dim inst = New MyObjectWithListOfIPs
inst.IPList = New List(Of String)(New String() {"10.0.0.1", "10.0.0.2", "10.0.0.3"})
Return inst
End Function
''' <summary>
''' Updates the ObservableCollection instance based on business object
''' </summary>
''' <remarks></remarks>
Private Sub RefreshIpAddresses()
_ipAddresses.Clear()
For Each ip As String In GetIpList().IPList
_ipAddresses.Add(ip)
Next
End Sub
Private Sub btnRefreshIPList_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles btnRefreshIPList.Click
RefreshIpAddresses()
End Sub
End Class
''' <summary>
''' Object with response data (e.g., list of IPs)
''' </summary>
''' <remarks></remarks>
Class MyObjectWithListOfIPs
Private _ipList As List(Of String)
Public Property IPList() As List(Of String)
Get
Return _ipList
End Get
Set(ByVal value As List(Of String))
_ipList = value
End Set
End Property
End Class
''' <summary>
''' Disperate object that "has an" address
''' </summary>
''' <remarks></remarks>
Class MyObjectWithIPAddress
Public Sub New(name As String)
Me._name = name
End Sub
Private _name As String
Public Property Name() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
Private _ipAddress As String
Public Property IPAddress() As String
Get
Return _ipAddress
End Get
Set(ByVal value As String)
_ipAddress = value
End Set
End Property
End Class

WPF AttachedCommandsBehavior in VB.NET

Can anybody give me an example how to implement AttachedCommands?
There are some examples in C# and I think it is very similar to C# but I have problems to translate the code to VB.NET.
At the moment my try to translate a C# AttachedCommand-Class to VB.net looks like that:
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.Windows
Imports System.Windows.Input
Imports System.Reflection
Public Class AttachedCommand
Inherits DependencyObject
#Region "CommandParameter-Class"
Private NotInheritable Class CommandParameter
Public Shared ReadOnly Property DefaultCP() As CommandParameter
Get
Return New CommandParameter()
End Get
End Property
Private _command As ICommand
Public Property Command() As ICommand
Get
Return _command
End Get
Set(ByVal value As ICommand)
_command = value
End Set
End Property
Private _eventName As String
Public Property EventName() As String
Get
Return _eventName
End Get
Set(ByVal value As String)
_eventName = value
End Set
End Property
Private _callBack As [Delegate]
Public Property Callback() As [Delegate]
Get
Return _callBack
End Get
Set(ByVal value As [Delegate])
_callBack = value
End Set
End Property
End Class
#End Region
#Region "Properties"
Private Shared _parameter As IDictionary(Of DependencyObject, CommandParameter)
Private Shared Property Parameter() As IDictionary(Of DependencyObject, CommandParameter)
Get
Return _parameter
End Get
Set(ByVal value As IDictionary(Of DependencyObject, CommandParameter))
_parameter = value
End Set
End Property
Public Property Command() As ICommand
Get
Return GetValue(CommandProperty)
End Get
Set(ByVal value As ICommand)
SetValue(CommandProperty, value)
End Set
End Property
Public Shared ReadOnly CommandProperty As DependencyProperty = _
DependencyProperty.Register("Command", _
GetType(ICommand), GetType(AttachedCommand), _
New UIPropertyMetadata(AddressOf CommandChanged))
Public Property EventName() As String
Get
Return GetValue(EventNameProperty)
End Get
Set(ByVal value As String)
SetValue(EventNameProperty, value)
End Set
End Property
Public Shared ReadOnly EventNameProperty As DependencyProperty = _
DependencyProperty.Register("EventName", _
GetType(String), GetType(AttachedCommand), _
New UIPropertyMetadata(AddressOf EventNameChanged))
#End Region
#Region "Event-Handler"
Public Shared Sub CommandChanged(ByVal dependencyObject As DependencyObject, ByVal args As DependencyPropertyChangedEventArgs)
If Not Parameter.ContainsKey(dependencyObject) Then
Parameter.Add(dependencyObject, CommandParameter.DefaultCP)
End If
If (Not Parameter(dependencyObject).Callback = Nothing) AndAlso (Not args.OldValue = Nothing) Then
_RemoveEventHandler(dependencyObject)
End If
Parameter(dependencyObject).Command = CType(args.NewValue, ICommand)
_AttachEventHandler(dependencyObject)
End Sub
Public Shared Sub EventNameChanged(ByVal dependencyObject As DependencyObject, ByVal args As DependencyPropertyChangedEventArgs)
If Not Parameter.ContainsKey(dependencyObject) Then
Parameter.Add(dependencyObject, CommandParameter.DefaultCP)
End If
End Sub
#End Region
#Region "Helper"
Private Shared Sub _RemoveEventHandler(ByVal dependencyObject As DependencyObject)
If dependencyObject Is Nothing Then
Throw New ArgumentNullException("DependencyObject is null.")
End If
If Not Parameter.ContainsKey(dependencyObject) Then
Exit Sub
End If
Dim param As CommandParameter = Parameter(dependencyObject)
If param.Callback Is Nothing Then
Throw New InvalidProgramException("Cannot remove Callback. Callback is null.")
End If
Dim eventInfo As EventInfo = dependencyObject.GetType().GetEvent(param.EventName)
If eventInfo Is Nothing Then
Throw New InvalidProgramException(String.Format("Cannot find an event with the name <{0}>", param.EventName))
End If
eventInfo.RemoveEventHandler(dependencyObject, param.Callback)
End Sub
Private Shared Sub _AttachEventHandler(ByVal dependencyObject)
If dependencyObject Is Nothing Then
Throw New ArgumentNullException("DependencyObject is null")
End If
If Not Parameter.ContainsKey(dependencyObject) Then
Exit Sub
End If
Dim param As CommandParameter = Parameter(dependencyObject)
If param.Command Is Nothing Or String.IsNullOrEmpty(param.EventName) Then
Exit Sub
End If
Dim eventInfo As EventInfo = dependencyObject.GetType.GetEvent(param.EventName)
If eventInfo Is Nothing Then
Throw New InvalidProgramException(String.Format("Cannot find an event with the name <{0}>", param.EventName))
End If
eventInfo.AddEventHandler(dependencyObject, param.Callback)
End Sub
Private Shared Sub _CommandExecutAction(ByVal parameter As CommandParameter)
parameter.Command.Execute(Nothing)
End Sub
Private Shared Function _CreateHandler(ByVal eventInfo As EventInfo, ByVal method As Action) As [Delegate]
If eventInfo Is Nothing Then
Throw New ArgumentNullException("EventInfo is null")
End If
If method Is Nothing Then
Throw New ArgumentNullException("Action-method is null")
End If
Dim handlerType = eventInfo.EventHandlerType
Dim eventParams = handlerType.GetMethod("Invoke").GetParameters()
Dim parameters = eventParams.[Select](Function(p) System.Linq.Expressions.Expression.Parameter(p.ParameterType, "x"))
Dim body = System.Linq.Expressions.Expression.[Call](System.Linq.Expressions.Expression.Constant(method), method.[GetType]().GetMethod("Invoke"))
Dim lambda = System.Linq.Expressions.Expression.Lambda(body, parameters.ToArray())
Return [Delegate].CreateDelegate(handlerType, lambda.Compile(), "Invoke", False)
End Function
#End Region
#Region "INIT"
Public Sub New()
Parameter = New Dictionary(Of DependencyObject, CommandParameter)()
End Sub
#End Region
End Class
But now I have the problem, that the GetValue() and SetValue()-methods are not available. Any ideas?
Thank you for your help..
An AttachedCommandBehaviour ain't much more than an dependency property of type ICommand, which hookes some events.
Check out Dr. WPFs VB-snippets that includes snippets for dependency properties.
If you use the one that includes a property changed handler, you can use this handler to hook the events you want. In the event handler, you call the attached command.
If you still can't make it work, please post some code.

Resources