Hierarchical Code First EF Binding to WPF Treeview... Impossible - wpf

In the past, I had to resort to using DataSets and DataTables, as doing it with Binding still eludes me...
Top Level: I created a series of classes in VB 2012, annotated them, and crated the EF model in EF6 using Code First. The idea is to represent router configs, with sub-sections of the config being children to the major sections. Very simple idea.
Rationale: With a simple WPF Treeview, illustrate the config sections and subsections as they appear (logically) in the router.
I have started very simply with these classes, intending later to use 2-way binding to update each way, etc. Here are the first 2 top classes (snipped for brevity):
This is standard EF CodeFirst fare, and the DBContext is laid out by EF as you'd expect. This is the layout of the top 2 level classes (Access_Group & Access_List):
...and all is well in EF-Land...
Because it will likely be important, here is the actual Access_Group class:
Public Class Access_Group
Inherits EntityTypeConfiguration(Of Access_Group)
Implements IAccess_Group
<Key>
Public Property Access_GroupID As Integer Implements IAccess_Group.Access_GroupID
Public Property Name As String Implements IAccess_Group.Name
Public Property LastUpdated As Date Implements IAccess_Group.LastUpdated
Public Property Active As Boolean Implements IAccess_Group.Active
'------------------------------------------------------------------
Public Property Access_Lists As ObservableCollection(Of Access_List) Implements IAccess_Group.Access_Lists
Public Sub New()
Me.Access_Lists = New ObservableCollection(Of Access_List)
End Sub
End Class
There are many event-based components that I have not added yet, because I just want the basics to work (display hierarchically in a Treeview) before I add the bells & whistles...
So this is how the classes are created in code, that populated the database (SQL 2012) in the first place:
[Window Class contd.]
Private Sub AddData()
Try
ctx = New entitiesContext
Dim d As Date = Now
'--------------------------------
Dim al As New Access_List
' lower classes not needed to be shown...
With al
.Active = True
.Checked = True
.LastUpdated = d
.Name = "some access-list at " & d.ToLongTimeString
End With
'--------------------------------
Dim ag As Access_Group = New Access_Group
With ag
.Access_Lists.Add(al)
.Active = True
.LastUpdated = d
.Name = "some access-group at " & d.ToLongTimeString
End With
'
ctx.Access_Groups.Add(ag)
'
Dim i As Integer = ctx.SaveChanges()
Console.WriteLine("Seed complete! -> " & i)
Catch ex As Exception
Dim exText As String = "Seed Failed "
Console.WriteLine(exText & "(Message): " & ex.Message)
Console.WriteLine(exText & "(ToString): " & ex.ToString)
Console.WriteLine(exText & "(StackTrace): " & ex.StackTrace)
Console.WriteLine("EntityValidationErrors: ")
For Each eve As System.Data.Entity.Validation.DbEntityValidationResult In ctx.GetValidationErrors()
Console.WriteLine("eve: OK? " & eve.IsValid & " - " & eve.Entry.ToString)
For Each devr As System.Data.Entity.Validation.DbValidationError In eve.ValidationErrors
Console.WriteLine("devr invalid property: " & devr.PropertyName)
Console.WriteLine("devr error message : " & devr.ErrorMessage)
Next
Next
End Try
End Sub
You see Access_List referred to above as the 2nd level down, and this is that class:
Public Class Access_List
Inherits EntityTypeConfiguration(Of Access_Group)
Implements toag.entities.IAccess_List
<Key>
Public Property Access_ListID As Integer Implements IAccess_List.Access_ListID
Public Property Name As String Implements IAccess_List.Name
Public Property LastUpdated As Date Implements IAccess_List.LastUpdated
Public Property Active As Boolean Implements IAccess_List.Active
Public Property Checked As Boolean Implements IAccess_List.Checked
Public Property Object_Groups As ObservableCollection(Of Object_Group) Implements IAccess_List.Object_Groups
Public Sub New()
Me.Object_Groups = New ObservableCollection(Of Object_Group)
End Sub
End Class
If I can figure out how to get these 2 classes to behave, I can template & get the rest to do so as well...
I have tried HUNDREDS of code & XAML combinations, so I'll settle down with one that at least shows something on the Treeview:
<TreeView Grid.Column="0"
x:Name="ACLTreeView"
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
ItemsSource="{Binding Access_Group}">
</TreeView>
When this XAML is combined with this code-behind:
Public Class ConfigWindow
Property ctx As entitiesContext
Public Access_Group_List As IEnumerable(Of Access_Group)
Sub New()
' This call is required by the designer.
InitializeComponent()
Startup()
End Sub
Public Sub Startup()
Try
ctx = New vASAContext
Me.Access_Group_List = From ag In ctx.Access_Groups Select ag
Me.ACLTreeView.ItemsSource = Access_Group_List.ToList
Catch ex As Exception
Debug.Print("ex: " & ex.Message)
End Try
End Sub
End Class
Will yield the following:
(sorry about having to obfuscate the namespace...) Which is fine, as there is no HierarchicalTemplate or even TreeViewItem in XAML.
Here is modified XAML:
...which will show the Name property of the Access_Group entity instead of it's class name [can't add a screenshot of it when editing a post, so you may have to trust me on this one! :)]
But there is another sub in the Window class that points to a problem with the hierarchy possibly not being recognized. Could it be that I've been trying examples that were correct, and my EF classes weren't set up properly? This sub should show all the elements and their children:
Public Sub PrintDebug(TheList As IEnumerable(Of Access_Group))
For Each ag As Access_Group In TheList
Console.WriteLine("=======================================")
Console.WriteLine("ag: " & ag.Name & " has " & ag.Access_Lists.Count & " Access_List entries")
For Each al As Access_List In ag.Access_Lists
Console.WriteLine("ag -> al: " & al.Name & " has " & al.Object_Groups.Count & " Object_Group entries")
For Each og As Object_Group In al.Object_Groups
Console.WriteLine("ag -> al -> og: " & og.Name & " has " & og.Network_Objects.Count & " Network_Object entries")
'...
Next
Next
Console.WriteLine("=======================================")
Next
End Sub
But this is what that debug class puts out:
=======================================
ag: some access-group at 5:00:49 PM has 0 Access_List entries
=======================================
=======================================
ag: some access-group at 5:08:56 PM has 0 Access_List entries
=======================================
=======================================
ag: some access-group at 5:09:14 PM has 0 Access_List entries
=======================================
=======================================
ag: some access-group at 5:12:31 PM has 0 Access_List entries
=======================================
[...]
? Does this mean that my Treeview doesn't have a chance? But, but... the data is correct in the DB:
All of those keys were populated by EF when I used the above code (only saving the top level class (Access_Group) after populating it's ObservableCollections...)
???
I've tried every combination of HierarchicalTemplate, in grid/window resources, nested, etc. And I'm back to square 1 after 3 days... :) Yes, all kinds of LINQ queries, too... And now I'm contemplating SQL (GASP) or JOINs in either LINQ/SQL, but then I may as well go all the way back to DataSets & DataTables if I'm ready to really give up...
Any help appreciated... I just can't move on until I can get these entities to bind correctly...

I've created sample object model of your entities (AccessGroup, AccessList and ObjectGroup) and this code might help you:
Code-behind
Imports System.Collections.ObjectModel
Class MainWindow
Property AccessGroups As New ObservableCollection(Of AccessGroup)
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Me.DataContext = Me
Dim ag1 = New AccessGroup With {.Name = "AG1"}
Dim ag2 = New AccessGroup With {.Name = "AG2"}
Dim al1 = New AccessList With {.Name = "AL1"}
Dim al2 = New AccessList With {.Name = "AL2"}
Dim al3 = New AccessList With {.Name = "AL3"}
Dim og1 = New ObjectGroup With {.Name = "OG1"}
Dim og2 = New ObjectGroup With {.Name = "OG2"}
al1.ObjectGroups = New List(Of ObjectGroup) From {og1}
al2.ObjectGroups = New List(Of ObjectGroup) From {og2}
ag1.AccessList = New List(Of AccessList) From {al1, al2}
ag2.AccessList = New List(Of AccessList) From {al3}
AccessGroups.Add(ag1)
AccessGroups.Add(ag2)
End Sub
End Class
Public Class AccessGroup
Property Name As String
Property AccessList As IEnumerable(Of AccessList)
End Class
Public Class AccessList
Property Name As String
Property ObjectGroups As IEnumerable(Of ObjectGroup)
End Class
Public Class ObjectGroup
Property Name As String
End Class
XAML
<TreeView ItemsSource="{Binding AccessGroups}">
<!-- AccessGroup template -->
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding AccessList}">
<TextBlock Text="{Binding Name}" />
<!-- AccessList template -->
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding ObjectGroups}">
<TextBlock Text="{Binding Name}" />
<!-- ObjectGroup template -->
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
On the picture bellow you can see the result

Related

Can't get email folder names using IMAPX

I am using this code to get the list of email folders :
Class emailFolder
Public Property Title As String
End Class
Public Shared Function GetFolders() As List(Of emailFolder)
Dim folder = New List(Of emailFolder)
Dim foldername = client.Folders
For Each parentFolder In foldername
Dim parentPath = parentFolder.Path
If parentFolder.HasChildren Then
Dim subfolders = parentFolder.SubFolders
For Each subfolder In subfolders
Dim subPath = subfolder.Path
folder.Add(New emailFolder With {.Title = parentFolder.Name})
Next
End If
Next
Return folder
End Function
Public sub btn_click handles Button1.click
ListView.ItemSource=GetFolders
I dunno what is wrong with my code but the items I get in the ListView (I'm in wpf by the way) look like this :
MyApplication++emailfolder
MyApplication++emailfolder
MyApplication++emailfolder
MyApplication++emailfolder
What am I doing wrong ?
If you define the ItemTemplate of the ListView, you can define how the ListViewItems should look like.
With the following example just the content of the property Title will be displayed:
<ListView>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Another approach is to add an override of the ToString-method to the emailFolder-class:
Class emailFolder
Public Property Title As String
Public Overrides Function ToString() As String
Return Me.Title
End Function
End Class
The problem was solved..Thanks for the comments guys!!
Just had to override the ToString....Full code:
Class emailFolder
Public Property Title As String
Public Overrides Function ToString() As String
Return Me.Title
End Function
End Class
Public Shared Function GetFolders() As List(Of emailFolder)
Dim folder = New List(Of emailFolder)
Dim foldername = client.Folders
For Each parentFolder In foldername
Dim parentPath = parentFolder.Path
If parentFolder.HasChildren Then
Dim subfolders = parentFolder.SubFolders
For Each subfolder In subfolders
Dim subPath = subfolder.Path
folder.Add(New emailFolder With {.Title = parentFolder.Name})
Next
End If
Next
Return folder
End Function
Public sub btn_click handles Button1.click
ListView.ItemSource=GetFolders

ItemsControl bound to ObservableColellection not updating UI on property change

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

DAL Generator: How to get Grid Checkbox?

I am building a WPF D.A.L. generator.
In my main page, I have a DataGrid populated with a list of the tables in my database.
I also have an extra checkbox column called ShouldInclude? I intend to use this to determine whether or not the table should be included in the generation... yes if checked, no if un-checked.
Because I am populating the DataGrid's ItemSource with a strongly typed list of some basic info TableName, Schema, Columns, I am now finding myself at a loss as to how I can get the checked value of the checkbox so I can make that determination on whether to include it or not.
Here are my functions that build out my table typing class code files:
Private Sub GenerateTyping(ByVal _DG As DataGrid)
For Each i As TableTyping In _DG.Items
'check if should be generated
Dim _TString As String = String.Empty
Using _sr As New StreamReader(Common.GetPath() & "Class Templates\CSharp\Typing\XXX_Typing.txt")
_TString = _sr.ReadToEnd()
_sr.Close()
End Using
Dim _FN As String = i.Name & "_Typing.cs"
Dim _Props As New StringBuilder()
Dim _CL As List(Of ColumnTyping) = i.Columns
For Each col In _CL
With _Props
Dim _PropStr As String = "public " & Common.GetClrType(col.Type) & " " & col.Name & " { get; set; }"
.AppendLine(" " & _PropStr)
End With
Next
'Write the new class files
_TString = _TString.Replace("##TABLENAME##", If(i.Schema.Length > 0, i.Schema & "_", "") & i.Name).Replace("##THE_PROPERTIES##", _Props.ToString())
If Not Directory.Exists(FilePath & "\Typing\") Then
Directory.CreateDirectory(FilePath & "\Typing\")
End If
Using _sw As New StreamWriter(FilePath & "\Typing\" & If(i.Schema.Length > 0, i.Schema & "_", "") & i.Name & "_Typing.cs", False)
_sw.Write(_TString)
_sw.Close()
End Using
_TString = String.Empty
_Props.Clear()
Next
End Sub
Partial Public Class TableTyping
Public Property ID As Integer
Public Property Name As String
Public Property Schema As String
Public Property Columns As List(Of ColumnTyping)
End Class
Partial Public Class ColumnTyping
Public Property ID As Integer
Public Property Name As String
Public Property Type As SqlDataType
Public Property Length As Integer
Public Property DefaultValue As String
Public Property Precision As Integer
Public Property Scale As Integer
End Class
My datagrid simply consists of 3 columns. Include?, Table Schema, Table Name, which is populated via:
<DataGrid EnableRowVirtualization="True" Name="dgTables" IsReadOnly="True" AutoGenerateColumns="False" FontFamily="Calibri" FontSize="14" IsEnabled="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Include?">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox Tag="{Binding ID}" HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="True" Name="ckTblInclude" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Binding="{Binding Schema}" Header="Schema"/>
<DataGridTextColumn Binding="{Binding Name}" Header="Name"/>
</DataGrid.Columns>
</DataGrid>
AND:
_tg.ItemsSource = _Table
Private Sub GrabTables()
Dim _Db As Database = Srv.Databases(DBName)
Dim _Tbls As TableCollection = _Db.Tables
Dim _tct As Integer = _Tbls.Count
Dim _i As Integer = 0
For i = 0 To _tct - 1
If Not _Tbls(i).IsSystemObject Then
_i += 1
_Tables.Add(New TableTyping() With {
.ID = _i,
.Name = _Tbls(i).Name,
.Schema = _Tbls(i).Schema,
.Columns = ProcessColumns(_Tbls(i).Columns)})
End If
Next
_TCount = _Tables.Count
End Sub
_Tables is a List(of TableTyping)
How can I do the Include? check inside the GenerateTyping procedure?
I had to change my checkbox template to the following:
<CheckBox Tag="{Binding ID}" HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding Include, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="ckTblInclude" />
And change the _Tables list to populate via:
Private Sub GrabTables()
Dim _Db As Database = Srv.Databases(DBName)
Dim _Tbls As TableCollection = _Db.Tables
Dim _tct As Integer = _Tbls.Count
Dim _i As Integer = 0
For i = 0 To _tct - 1
If Not _Tbls(i).IsSystemObject Then
_i += 1
_Tables.Add(New TableTyping() With {
.ID = _i,
.Name = _Tbls(i).Name,
.Schema = _Tbls(i).Schema,
.Columns = ProcessColumns(_Tbls(i).Columns),
.Include = True})
End If
Next
_TCount = _Tables.Count
End Sub

Sorting groups based on Group ItemCount

On a ListCollectionView, i just add new GroupDescriptions.
But i am looking a way to sort them based on the ItemCount of the groups.
So at first position i would get the group with the most items.
You could wrap the groups in another view and sort that.
XAML example:
<CollectionViewSource x:Key="Items2" Source="{Binding Groups,
Source={StaticResource Items}}">
<CollectionViewSource.SortDescriptions>
<cm:SortDescription PropertyName="ItemCount" Direction="Descending"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
I've created a custom ListCollectionView to make top level group sorting possible, using a great library System.Linq.Dynamic:
Public Class ListCollectionViewEx
Inherits ListCollectionView
Private _groupSortDescriptions As SortDescriptionCollection
Private _groups As ReadOnlyObservableCollection(Of Object)
Public Overridable ReadOnly Property GroupSortDescriptions As SortDescriptionCollection
Get
Return Me._groupSortDescriptions
End Get
End Property
Public Overrides ReadOnly Property Groups As System.Collections.ObjectModel.ReadOnlyObservableCollection(Of Object)
Get
If Me._groupSortDescriptions.Count > 0 Then
If Me._groups Is Nothing AndAlso
MyBase.Groups IsNot Nothing Then
Dim qs As String = String.Join(
",",
Me._groupSortDescriptions.
Select(Function(gsd) String.Format("{0} {1}", gsd.PropertyName, If(gsd.Direction = ListSortDirection.Ascending, "ASC", "DESC"))).
ToArray)
Dim sortedGroups = MyBase.Groups.
Select(Function(g) DirectCast(g, CollectionViewGroup)).
AsQueryable.OrderBy(qs).
AsEnumerable
If sortedGroups IsNot Nothing AndAlso
sortedGroups.Count > 0 Then
Me._groups = New ReadOnlyObservableCollection(Of Object)(New ObservableCollection(Of Object)(sortedGroups))
End If
End If
Return Me._groups
End If
Return MyBase.Groups
End Get
End Property
Public Sub New(collection As IEnumerable)
MyBase.New(collection)
Me._groupSortDescriptions = New SortDescriptionCollection()
End Sub
End Class
Usage:
View.GroupSortDescriptions.Add(New SortDescription("ItemCount", ListSortDirection.Descending))

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

Resources