in

vbCity Blogs

New (temp) place for vbCity Blogs

Ged Mead's Blog

  • WPF: How To Display Master-Detail Data In a TreeView (Part 2)

     

     

    Introduction
    In this earlier blog I used an XML file containing nested data items as the data source of a TreeView. In this version I will use a collection of objects as the data source.
    The details of the data are much the same as I used in the previous blog, with a couple of tiny changes to avoid any possible confusion caused by the use of Visual Basic keywords as field names:

    These classes are saved in a VB file named SalesData.vb. Because it is fairly lengthy, filling about three screen display lengths, I've made it available from this link. (To a large extent, how the the data is created isn't really the topic of this blog - I'm more concerned with the Binding to a source and the templates used for displaying the data.)

    Mapping to the local Assembly
    In order to be able to access those SalesPerson, SalesOrder, etc classes and objects in the XAML file, it is necessary to create a mapping. The syntax for this is as follows:  

      xmlns:local="clr-namespace:HierarchicalDataTemplate" 

    It isn't mandatory to use 'local' as the mapping alias, but it's a fairly traditional approach. In this case, of course 'HierarchicalDataTemplate' is the name of the project I am working on.

    With the namespace mapping in place, any of the classes that currently exist in the code-behind files of the project become visible to the XAML file. If you view the code listing for the SalesData.vb file, you will see that the class which creates the demo data is called 'SalesPersonList'. It's now possible to create a new instance of that class in the XAML file : 

        <local:SalesPersonList x:Key="SalesPersonList"/> 

    I generally place this in the Window.Resources collection.

    Creating a TreeView and Binding its Data Source
    In the markup for the Window itself, I'll create a TreeView which contains a single TreeViewItem.  

        <TreeView>

          <TreeViewItem ItemsSource="{Binding Source={StaticResource SalesPersonList}}"

              Header="Sales Figures" />

        </TreeView> 

    The ItemsSource is the crucial property here. It identifies exactly where the TreeView should look for its data. In this case it looks into the SalesPersonList instance that I created a few moments ago. For the avoidance of doubt, the exact 'SalesPersonList' that is used is the StaticResource created earlier and identified by the Key. (I possibly should have used a different name for the Key and the underlying Class to avoid any confusion).

    Because of the use of the Binding and the HierarchicalDataTemplates, it is only necessary to create this single TreeViewItem. The Binding engine will trawl through all the data and create as many TreeViewItem nodes as it needs to in order to display everything correctly. 

    HierarchicalDataTemplates
    This example uses three HierarchicalDataTemplates, plus a standard DataTemplate for the SalesItems. Here is the first one (which again I've placed in the Window.Resources collection):  

        <HierarchicalDataTemplate DataType="{x:Type local:SalesPerson}"

              ItemsSource="{Binding Path=Periods}">

          <TextBlock Text="{Binding Path=Name}"/>

        </HierarchicalDataTemplate> 

    The DataType property identifies which type of object will be dealt with by this template. In the case of this first template, this will be the SalesPerson type. In other words, when the Binding that I placed on the TreeViewItem finds any instance of a SalesPerson inside that SalesPersonList, it will look at this template to discover how it should display SalesPerson details.
    In this case, the Name property of the current SalesPerson will be shown in a TextBlock.
    The ItemsSource property also uses a Binding, but it is only interested in knowing what needs to be shown as the child data of SalesPersons. As we know from the diagram above, this will be the Periods data.

    If you are finding the DataType and the Bindings' Paths a bit tricky to grasp, it may help if you run the project as it currently stands. Here's the markup for the Window so far: 

    <Window x:Class="OrdersListsDisplay"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       xmlns:local="clr-namespace:HierarchicalDataTemplate"

       Title="Sales List Display" Height="300" Width="300">

      <Window.Resources>

        <local:SalesPersonList x:Key="SalesPersonList"/>

     

        <!--  Data Templates -->

        <HierarchicalDataTemplate DataType="{x:Type local:SalesPerson}"

              ItemsSource="{Binding Path=Periods}">

          <TextBlock Text="{Binding Path=Name}"/>

        </HierarchicalDataTemplate>

     

      </Window.Resources>

        <Grid>

        <TreeView>

          <TreeViewItem ItemsSource="{Binding Source={StaticResource SalesPersonList}}"

              Header="Sales Figures" />

        </TreeView>

      </Grid>

    </Window> 

    When you run this, you will first see:

    Then, when you expand the first node, you will have:

    Finally, clicking on either or both the SalesPerson nodes, you will see that the application has tried to display the children of the SalesPersons for you. In the absence of any instruction about formatting the Periods, it simply reverts to displaying the default ToString rendering of the class.

    Hopefully though you can now see why the template points to SalesPerson as its DataType, but identifies the next level down the tree as the ItemsSource. (You may have noticed that this wasn't necessary with the XML data example in the earlier blog, where simply assigning a Binding without a Path will work.)

    Displaying Periods and SalesOrders
    Exactly the same approach is used for the next two templates: 

        <HierarchicalDataTemplate DataType="{x:Type local:Period}"

              ItemsSource="{Binding Path=SalesOrders}">

          <TextBlock Text="{Binding Path=Name}"/>

        </HierarchicalDataTemplate>

     

        <HierarchicalDataTemplate DataType="{x:Type local:SalesOrder}"

             ItemsSource="{Binding Path=SalesItems}">

          <TextBlock Text="{Binding Path=Name}"/>

        </HierarchicalDataTemplate> 

    The three HierarchicalDataTemplates in place so far will produce this result:

    which should come as no surprise, based on what I explained earlier.

    ItemDetail DataTemplate
    So that just leaves the template for the SalesItems and ItemDetail data. This time a standard DataTemplate will do, because we know there is no further data below ItemDetail.  

        <DataTemplate DataType="{x:Type local:SalesItem}">

          <TextBlock Text="{Binding Path=ItemDetail}"/>

        </DataTemplate> 

    Just for the record, a HierarchicalDataTemplate would work but isn't necessary. With the final DataTemplate added, every level of the Master-Detail data can be accessed:

    Formatting
    You have many options for formatting the final presentation of the data. In this example, you can change the color, font size and weight, indentation, etc via the individual TextBlock properties. If you wanted to insert additional graphical detail (such as an icon), you simply wrap the TextBlock in a StackPanel with its Orientation set to Horizontal. You can then insert the icon image as an additional child of this StackPanel.

    You can build on this basic example shown here to create a much more complex display.

  • WPF HierarchicalDataTemplates and Master-Detail Data Display

     Introduction
    For some reason, I struggled with HierarchicalDataTemplates when I first tried to use them. Although I understood the overall theory that they walk the data tree and can create formatted, nested output, I always seemed to spend an inordinately long time trying to get things to work just as I want them (or sometimes, to be truthful, to work at all!)  

    So I thought I would work through a few samples over the space of a few blogs, building up the complexity as I go. Hopefully, if you too have come unstuck using these kind of templates, this step by step approach might be helpful to you.

    Some Data
    I'll start by using some XML data. This has the advantage that you can physically see the tree-like composition of the data. Here is a schematic of the overall structure of the data:

     

        

     

      

    As you can see from the diagram, each Salesperson can have several children named 'Period', each Period can have several children named ''Order' and each Order can have several children named 'Item'.

    To keep things short and simple, I have only included two SalesPerson instances in the demo file. Here is the content of the XML data file:  

    <SalesFigures xmlns="">

      <SalesPerson Name="Jean Price">

     

        <Period PeriodName="January">

          <Order OrderNumber="Order # JAN001">

            <Item>500 Widgets</Item>

            <Item>120 Gizmos</Item>

          </Order>

          <Order OrderNumber="Order # JAN002">

            <Item>200 Wiggles</Item>

            <Item>20 Schmoos</Item>

            <Item>312 Linguines</Item>

          </Order>

        </Period>

     

        <Period PeriodName="February">

          <Order OrderNumber="Order # FEB001">

            <Item>1000 Widgets</Item>

            <Item>76 Schmoos</Item>

          </Order>

        </Period>

      </SalesPerson>

     

    <!-- Second SalesPerson -->

      <SalesPerson Name="John P Grant">

        <Period PeriodName="January">

          <Order OrderNumber="Order # JAN001">

            <Item>200 Widgets</Item>

            <Item>120 Woggles</Item>

          </Order>

          <Order OrderNumber="Order # JAN002">

            <Item>100 Wiggles</Item>

            <Item>222 Linguines</Item>

          </Order>

        </Period>

     

        <Period PeriodName="February">

          <Order OrderNumber="Order # FEB001">

            <Item>75 Smashies</Item>

            <Item>176 Widgets</Item>

            <Item>750 Small Blingshies</Item>

            <Item>110 Scoobs</Item>

          </Order>

        </Period>

      </SalesPerson>

     

    </SalesFigures> 

    Accessing the XML data
    When using XML data as the source, you create an XMLDataProvider in the markup. This usually consists of three elements:

    • A Key, which you can use later to refer to it.
    • A Source, which points to the location of the data (the XML file)
    • An XPath, which identifies the starting point of the data tree.  

    The XMLDataProvider can be placed in the Resources collection of the Window: 

      <Window.Resources>

        <XmlDataProvider x:Key="SalesInfo"

           Source="SalesStats.xml"

           XPath="/SalesFigures"></XmlDataProvider>

      </Window.Resources> 

    The Templates
    The key pieces of HierarchicalDataTemplates are the DataType property and the ItemsSource property. The following template is also stored in the Window.Resources collection:  

        <HierarchicalDataTemplate DataType="SalesPerson"

            ItemsSource ="{Binding}">

          <TextBlock Text="{Binding XPath=@Name}" />

        </HierarchicalDataTemplate> 

    The DataType identifies the Type to which this template will be applied. Thanks to the magic of WPF DataBinding, this is done almost automatically - that is to say that whenever an instance of the target DataType is found as the binding engine does its work, this template will be used to display it. Our XML data file contains a type named SalesPerson and each time one is found, the template will be used. The ItemsSource property is easy to use in this case and requires only the inclusion of the 'Binding' extension as shown.

    Specifically in this case the template will employ a TextBlock to display the Name of the particular Salesperson instance. Because the data is XML based, XPath is used in place of the usual Path syntax and the @ symbol is used to signify that the binding needs to look for an attribute rather than an element.

    First Test Run
    In order to see the result so far, we'll add a TreeView to the WPF Window. 

        <TreeView >

          <TreeViewItem

            ItemsSource="{Binding Source={StaticResource SalesInfo}, XPath=*}"

           Header="SalesFigures"></TreeViewItem>

        </TreeView> 

    Note the use of a single TreeViewItem and the Binding Source being set to that XMLDataProvider. The XPath setting uses the asterisk notation, which means that the search will begin from the start of the data source (Or from the top of the tree, if you prefer to think of it in those terms).

    If you run this project as-is, it starts out as you would expect:

    Both the Salesperson instances are shown, each on its own node. However, if you expand either of those SalesPersons you will be less pleased with the result:

    The reason for this result is that the data template is targeted specifically at the Name attribute of SalesPerson instances and the TreeView doesn't really know how to display anything else it subsequently finds as it traverses deeper into the data. As you can see, it ignores any Attributes, but concatenates all the Elements - in this case the Items elements - it finds.

    The answer of course is to create templates for each level and in this example this requires two more HierarchicalDataTemplates. Again, these are placed in the Window.Resources. 

       <HierarchicalDataTemplate DataType="Period"

          ItemsSource ="{Binding}">

          <TextBlock Text="{Binding XPath=@PeriodName}" />

        </HierarchicalDataTemplate>

     

        <HierarchicalDataTemplate DataType="Order"

          ItemsSource ="{Binding}" >

          <TextBlock Text="{Binding XPath=@OrderNumber}" />

        </HierarchicalDataTemplate> 

    The approach used is exactly the same as for the first template - identify the DataType, set the ItemsSource to start searching from the top of the bound data tree and use a TextBlock for the display.

    Of course, you can format the text - e.g. changing the font weight or color and so on - just by setting properties on the individual TextBlocks in the Data Templates:

     

    I'll look a bit deeper into this in future blogs, including looking at other kinds of data sources.   You can read the one that creates a List collection of SalesPerson objects and displays the Master-Detail data here. 

     

  • A Better WPF TreeViewItem Grouping Sample

     In previous blogs I was looking at ways of grouping TreeViewItems. Those blogs included other features, such as changing the visibility of individual nodes based on the data content of each field. The blog that dealt with TreeViewItem visibility is here and the one that expanded the idea to include grouping is here. Although it worked to an extent, I wasn't really happy with the final result and you know how that can niggle at you - especially if you have gone public with it! So I've done some more research and now have a version that looks right.

    In this blog I'm going to leave out the ValueConverter/Visibility feature, as it tends to get in the way of the core layout steps. I will post up a second version later that does include this.

    I'm indebted to Bea Costa for laying the groundwork on this in a blog she wrote back in the days when WPF was still at the Orcas Beta stage. I've tweaked the original a little and converted the C# code to Visual Basic.

    Here is the finished result we are looking for:

    The Data
    As with the earlier blogs, I'm using a Person class which has three properties - FullName, Status and Category. The plan is to have FullName on one line of a TreeView node with Status immediately below it on a second line. Each of these two line sets will be grouped according to their Category property value.

    Here's the code for the Person class:

     

    Imports System.ComponentModel

     

    Public Class Person

        Implements INotifyPropertyChanged

     

        Sub New(ByVal personname As String, ByVal personstatus As String, ByVal personcategory As String)

            Me.FullName = personname

            Me.Status = personstatus

            Me.Category = personcategory

        End Sub

     

        Private _name As String

        Public Property FullName() As String

            Get

                Return _name

            End Get

            Set(ByVal value As String)

                _name = value

                OnPropertyChanged(New PropertyChangedEventArgs("FullName"))

            End Set

        End Property

     

        Private _status As String

        Public Property Status() As String

            Get

                Return _status

            End Get

            Set(ByVal value As String)

                _status = value

                OnPropertyChanged(New PropertyChangedEventArgs("Status"))

            End Set

        End Property

     

        Private _Category As String

        Public Property Category() As String

            Get

                Return _Category

            End Get

            Set(ByVal value As String)

                _Category = value

                OnPropertyChanged(New PropertyChangedEventArgs("Category"))

            End Set

        End Property

     

        Public Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs)

            If Not PropertyChangedEvent Is Nothing Then

                RaiseEvent PropertyChanged(Me, e)

            End If

        End Sub

     

        Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

     

    End Class

     

    In tis version, I introduce a Persons class. This replaces the Shared method to generate some dummy data that I was using in previous versions. The end result is the same - a List (Of Person) that can be used as the data source for the TreeView.

    Here's the code for that class:

     

    Public Class Persons

     

        Private _personlist As List(Of Person)

     

        Public ReadOnly Property PersonList() As IEnumerable(Of Person)

            Get

                Return _personlist

            End Get

        End Property

     

     

        Public Sub New()

            _personlist = New List(Of Person)()

            _personlist.Add(New Person("Ged Mead", "Busy", "VB City"))

            _personlist.Add(New Person("Joe Brown", "On Site", "Work Colleagues"))

            _personlist.Add(New Person("Serge Baliansky", "North Office", "VB City"))

            _personlist.Add(New Person("Fran Pickman", "East Centre", "Family"))

            _personlist.Add(New Person("Elaine Jones", "Not Available", "Work Colleagues"))

            _personlist.Add(New Person("Matt Bianca", "South Office", "VB City"))

        End Sub

    End Class

     

    TreeView UserControl
    To remain consistent with the original blogs, which created a WPF UserControl that can be hosted in a Windows Forms project, I'll build a UserControl here too. Of course, if you want to have this grouped TreeView in an all-WPF project then you can simply build it inside a Window.

    As before, it is necessary to map the assembly to an XML namespace in the XAML pane so that you can access the Persons data.

     

       xmlns:local="clr-namespace:GroupingTest"

     

    With that in place, an instance of the Persons class can be created in the XAML markup. This is stored in the UserControl.Resources collection:

     

      <UserControl.Resources>

        <local:Persons x:Key="People" />

     

    (There are more items to go into that Resources collection yet, so I haven't closed it off).

    CollectionView
    In the earlier blogs, I created the CollectionView and added a new PropertyGroupDescription in the code-behind. There is an example and a description of how CollectionView works here. This time I'm going to use a XAML version of the same thing. It makes things slightly easier because the Persons instance has already been created in XAML in the Resources collection too.

     

        <CollectionViewSource x:Key="cvs" Source="{Binding Source={StaticResource People}, Path=PersonList}">

          <CollectionViewSource.GroupDescriptions>

            <PropertyGroupDescription PropertyName="Category"/>

          </CollectionViewSource.GroupDescriptions>

        </CollectionViewSource>

     

    If you compare this to the code-behind VB version, the mapping is very clear. The Binding in the first line of the markup may look a bit complex at first, but essentially it :

    • Finds the Persons instance via the key that was assigned to it in the line above.
    • Then within that Persons instance it points the Path to the list of Person objects that are stored in the PersonList collection.

     

    DataTemplates
    Now that the data exists and is accessible, the next step is to create the DataTemplates. There will be two of them and the order in which you place them in the XAML file is important.

    The first template will be for the child items - that is, the two lines that contain the FullName and the Status property values. Here is the markup which creates it (again this can be stored in the UserControl.Resources collection):

     

        <DataTemplate x:Key="PersonandStatusTemplate">

          <StackPanel>

            <TextBlock Text="{Binding Path=FullName}"/>

            <TextBlock Text="{Binding Path=Status}"

             Padding="0,0,0,3" Foreground="DarkGreen"/>

          </StackPanel>

        </DataTemplate>

     

    This is pretty much the same template as used in the previous blogs. I've just tweaked a bit of the formatting.

    The key change to making the grouping work properly is the next template, a HierarchicalDataTemplate:

     

        <HierarchicalDataTemplate x:Key="categoryTemplate"

               ItemsSource="{Binding Path=Items}"

               ItemTemplate="{StaticResource PersonandStatusTemplate}">

          <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"

                    Margin="0,5,0,0"/>

        </HierarchicalDataTemplate>

     

    The ItemsSource points to the specific data to be shown via this template and in this case it is the collection of items stored in the PersonList.
    The ItemTemplate is used for the display of the children (the FullName and Status values) and points to the DataTemplate named PersonandStatusTemplate. This is why the order of elements in the XAML file is important. PersonandStatusTemplate must pre-exist in order for the HierarchicalDataTemplate to find it.

    The Binding Path for the TextBlock Text has the potential to mightily confuse. As you see, it points to 'Name'. As there is no 'Name' field in the data source this may seem strange. 'Name' in this context points to the name assigned to the PropertyGroupDescription in the CollectionViewSource earlier in the XAML file. (If you want a more detailed explanation of what is going on, you can read this earlier blog - the relevant part being somewhere towards the end.)

    So, the logic of this setup is that

    • An instance of the Persons class is created
    • This instance contains a collection of Person objects.
    • A PropertyGroupDescription in a CollectionViewSource identifies the 'Category' property as the value on which it will group items.
    • A DataTemplate will format the look of the FullName and Status fields of each item.
    • A HierarchicalDataTemplate recurses through the items, groups them according to its PropertyGroupDescription (Category, in this case) and displays the children using the PersonandStatusTemplate.

     

    Using the UserControl
    If you use this UserControl in a WPF project, you need to map the assembly to an XML namespace - something you will be coming quite familiar with if you have tried any of my earlier blogs! You can then create a new instance of the UserControl that I have named GTV.

     

    <Window x:Class="Window3"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       xmlns:local="clr-namespace:GroupingTest"

       Title="TreeView UserControl In Window" Height="400" Width="330">

        <Grid>

        <local:GTV Width="160" Margin="0,15" />

     

      </Grid>

    </Window>

     

    As I mentioned earlier, this TreeView isn't so complicated that it deserves UserControl status within a WPF project and I have only taken this route because it was originally created in order to be housed in a WinForms project. If you need to use this in a WinForms project, you can follow the steps shown in the earlier blogs and replace the UserControl with this one.

    You can download the sample WPF project from here. It contains two Windows: one uses the UserControl, the other builds the TreeView inside the Window itself.

    So I'm happy now with the look and functionality of the grouped TreeView. It has taken more work and research than I expected, mostly because I had previously found it quite hard to get to grips with HierarchicalDataTemplates. I hope that this example and description will help anyone else who has also struggled with it.

  • Grouping the Two Line TreeView Items

     In this earlier blog I described how to change the visibility of a line in a TreeView which has TreeViewItems that consist of two lines each. The original requirement also included grouping the entries under the different Categories. I purposely didn't include that at the time, because I thought it would obscure the main topic - i.e. changing the visibility. So in this item I will just expand the original project to include grouping.

    The first step is to create a new View using ICollectionView. This is in the System.ComponentModel namespace, so I will use an Imports statement for this.
    The easiest way to get the view is to use the GetDefaultView method of CollectionViewSource. This can be found in the System.Windows.Data namespace.
    Here is the code:  

    Imports System.ComponentModel

    Imports System.Windows.Data

     

    Partial Public Class TwoLineTreeView

        Dim Contacts As New List(Of Person)

     

     

        Private Sub TwoLineTreeView_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

            Contacts = Person.GetPersons

            Me.DataContext = Contacts

     

     

            Dim currentView As ICollectionView

            currentView = CollectionViewSource.GetDefaultView(Contacts)

     

            '  Group by category

            currentView.GroupDescriptions.Add(New PropertyGroupDescription("Category"))

     

        End Sub

     

    End Class 

    If you want more detail of how CollectionView works, you might want to read my blog here. The key point is that a CollectionView is created automatically behind the scenes when you set up the Binding.

    Once you make this view available, you can use it in the GroupStyle of the TreeView. Specifically, you create a new DataTemplate for the HeaderTemplate. Here is the markup for this part of the TreeView:  

          <TreeView.GroupStyle>

            <GroupStyle>

              <GroupStyle.HeaderTemplate>

                <DataTemplate>

                  <TreeViewItem   IsExpanded="True"> 

                  <TextBlock Text="{Binding Path=Name}"

                    Margin="2"   FontWeight="Bold"/>

                    </TreeViewItem>

                </DataTemplate>

              </GroupStyle.HeaderTemplate>

            </GroupStyle>

          </TreeView.GroupStyle> 

    The only part of this markup that might confuse you is the use of a Path value of 'Name' in the TextBlock.Text property. This isn't Name, as in the name of any of the Person instances, but represents the name given to the PropertyGroupDescription when it was added to the current view's GroupDescriptions.

    When you run the project, each of the Person instances is grouped under the heading of their Status value:

    I'm not entirely happy with the blank line between each TreeViewItem.  I suspect this is caused because there is no value being directly assigned to the TreeViewItem's Header property.    This causes an empty header to be displayed.      I'm looking into this and will post a follow up when I find the answer.

    You can access the demo project used for this blog from here.

     

  • Controlling TreeView Item Visibility

    An interesting question came up recently, where someone wanted to have a TreeView in which:

    • Each TreeViewItem consisted of two separate lines of content
    • but the second line may be hidden, depending on its content.  

    This was actually a Windows Forms question, but I wondered if this would be a good candidate for harnessing the graphical power of WPF to solve a tough WinForms problem. My idea was to create a WPF UserControl that had the features described above and then host this User Control in an ElementHost in the Windows Form.

    It seemed like a reasonable proposition and here's how I went about it.

    Data Source
    First of all I need some data, so will use a simple Person class.  

     

    Imports System.ComponentModel

    Imports System.Collections.ObjectModel

     

    Public Class Person

        Implements INotifyPropertyChanged

     

        Sub New(ByVal personname As String, ByVal personstatus As String, ByVal personcategory As String)

            Me.FullName = personname

            Me.Status = personstatus

            Me.Category = personcategory

        End Sub

     

        Private _name As String

        Public Property FullName() As String

            Get

                Return _name

            End Get

            Set(ByVal value As String)

                _name = value

                OnPropertyChanged(New PropertyChangedEventArgs("FullName"))

            End Set

        End Property

     

     

        Private _status As String

        Public Property Status() As String

            Get

                Return _status

            End Get

            Set(ByVal value As String)

                _status = value

                OnPropertyChanged(New PropertyChangedEventArgs("Status"))

            End Set

        End Property

     

     

        Private _Category As String

        Public Property Category() As String

            Get

                Return _Category

            End Get

            Set(ByVal value As String)

                _Category = value

                OnPropertyChanged(New PropertyChangedEventArgs("Category"))

            End Set

        End Property

     

     

        Public Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs)

            If Not PropertyChangedEvent Is Nothing Then

                RaiseEvent PropertyChanged(Me, e)

            End If

        End Sub

     

     

        Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

     

     

        Public Shared Function GetPersons() As List(Of Person)

            Dim GP As New List(Of Person)

            GP.Add(New Person("Ged Mead", "Busy", "VB City"))

            GP.Add(New Person("Joe Brown", "On Site", "Work Colleagues"))

            GP.Add(New Person("Serge Baliansky", "North Office", "VB City"))

            GP.Add(New Person("Fran Pickman", "East Centre", "Family"))

            GP.Add(New Person("Elaine Jones", "Not Available", "Work Colleagues"))

            GP.Add(New Person("Matt Bianca", "South Office", "VB City"))

            Return GP

        End Function

     

    End Class

     

    As you see, this class implements INotifyPropertyChanged, so that changes to instance properties can be caught and any appropriate action taken as a result. I also included a basic method to generate some dummy data.

    The Basic TreeView
    The TreeView will be a WPF UserControl, so it is necessary to create this inside the Windows Forms project. Use the Project > Add New Item menu for this, then select WPF from the selection window that appears and name the WPF UserControl:

       

    In the code-behind, you create a DataContext which will be the source of the data that is used to populate the TreeView:

    Partial Public Class TwoLineTreeView

        Dim Contacts As New List(Of Person)

     

     

        Private Sub TwoLineTreeView_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

            Contacts = Person.GetPersons

            Me.DataContext = Contacts

        End Sub

     

    End Class 

    An easy way to package up the two line content display is to use two TextBlocks inside a StackPanel. These would all be stored in a DataTemplate that the TreeView will use for its ItemTemplate property. Here's the first pass at the DataTemplate which I have placed in the Resources section of the UserControl named TwoLineTreeView. 

      <UserControl.Resources>

         <DataTemplate x:Key="TVVis" >

          <TreeViewItem IsExpanded="True">

     

            <StackPanel>

              <TextBlock Text="{Binding Path=FullName}"

                      Margin="2,0,0,1"></TextBlock>

              <TextBlock Text="{Binding Path=Status}"

                      Margin="8,3,2,2"></TextBlock>

            </StackPanel>

          </TreeViewItem>

        </DataTemplate>

      </UserControl.Resources> 

    The first TextBlock binds to the Person's name and the second one binds to their Status.

    We can test this early stage by adding a TreeView control to the UserControl: 

      <Grid>

        <TreeView ItemsSource="{Binding}"

             ItemTemplate="{StaticResource TVVis}"   >

        </TreeView>

     

      </Grid> 

    The Grid is inserted in the UserControl by default and although we don't really need it in this example there's no great value in deleting it.

    If you now move to the Windows Form, you can add an ElementHost and populate it with this data bound user control. You can either find the ElementHost control in the Toolbox and drag an instance of it on to the surface of the form. If it hasn't been added to the Toolbox (and as it is a Windows Forms project it doesn't always appear by default) you can simply find the UserControl itself in the Toolbox and drag this on to the Form.

       

     

    When you do so, an ElementHost will be created for you on the Form and will automatically offer you the TwoLineTreeView user control as the Child of that ElementHost. Run the project and you will see:

       

    Enabling and Disabling Visibility of the Second TextBlock

    The easiest way of hiding or showing the second item of data (the status of the Person) is to use a ValueConverter. I've written several blogs describing the use of these and you can check them out if you need more detail on how they work. An introduction to a simple ValueConverter is covered here and a more complex verion here and here.

    Here is the Converter class used in this project: 

    Imports System.Windows.Data

     

    Public Class StatusToVisibilityConverter

        Implements IValueConverter

     

        Public Function Convert(ByVal value As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert

            Select Case value.ToString

                Case "Busy", "Not Available", "En Route"

                    Return "Collapsed"

               

                Case Else

                    Return "Visible"

            End Select

        End Function

     

        Public Function ConvertBack(ByVal value As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack

            Throw New NotImplementedException()

        End Function

    End Class 

    It tests the value of the Status property and if it finds it is set to Busy, Not Available or En Route it will not display the line. It will display it in all other cases.

    The UserControl requires a namespace mapping before we can use this class to create and use a converter in the XAML pane.

       xmlns:local="clr-namespace:HostedTreeView"

    With that in place, a converter is created inside the UserControl's Resources collection: 

        <local:StatusToVisibilityConverter x:Key="StatusConverter" />

     

    The final step is to use the converter in the markup for the second TextBlock, specifically to hide or show it based on the content. Here is the amended TextBlock markup: 

             <TextBlock Text="{Binding Path=Status}"

               Visibility="{Binding Path=Status, Converter={StaticResource StatusConverter}}" 

               Margin="8,3,2,2"></TextBlock> 

    Now when the project is run, the two Person instances that contain Busy or Not Available as their Status will not display the second line.

     

       

    You can of course take this basic approach and use alternatives or weave a much more complex set of interconnections - perhaps replacing the Status text with another message altogether or totally hiding a Person and their Status if their Status has a particular value. Or whatever other variation meets your needs.  They key task was to create a two line node in which you could control the visibility of the lines.

    You can download the project used in this blog from here.

  • Create and Use a WPF Custom Command - Follow-Up

     

     

    In this earlier blog, I explained how you can create and use a Command to carry out a task based on a variety of user actions. These actions included key presses, mouse actions and key/mouse combinations, as well as clicking on WPF controls. At the time I said that if you didn't want to have the Image move unless the right mouse button was clicked directly on the Image itself (as opposed to clicking anywhere in the Window or on its other children) then you could use Bubbling and Tunneling. When I came to test this theory, however, I couldn't find an obvious way of achieving this. I could stop a right click on the Image working, but that was the exact opposite of what I was trying to do!

    Whether this failure is due to the way the Command infrastructure works or (more likely) my inability to grasp the finer nuances of routing, I've had to admit defeat - at least temporarily. So I thought I should at least post up a workaround for anyone who read that blog item and did want the right click to work only on the Image itself.

    The 'fix', such as it is, is to ditch the InputGesture for MouseAction.RightClick in the InputGestureCollection and use a standard event handler purely for the Image MouseDown event.

    So, the MoveItCommand code becomes:

     

    Public Class MoveItCommand2

     

        Private Shared _moveit As RoutedUICommand

        Public Shared ReadOnly Property MoveIt() As RoutedUICommand

            Get

                Return _moveit

            End Get

        End Property

     

        Shared Sub New()

            '  Add keyboard and mouse gestures

            Dim UserInputs As New InputGestureCollection()

            UserInputs.Add(New KeyGesture(Key.M, ModifierKeys.Alt))

            UserInputs.Add(New KeyGesture(Key.F12, ModifierKeys.None))

            ' UserInputs.Add(New MouseGesture(MouseAction.RightClick, ModifierKeys.None))

            UserInputs.Add(New MouseGesture(MouseAction.LeftClick, ModifierKeys.Shift))

            '  Assign these gestures to the _moveit field (and thereby to the MoveIt property)

            _moveit = New RoutedUICommand("Move Element", "Move", GetType(MoveItCommand), UserInputs)

        End Sub

     

    End Class

     

    You'll notice that I've renamed the class to MoveItCommand2.

    The code behind for the Window now takes an event handler for the Image MouseDown:

     

        Private Sub MoveableImage_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles MoveableImage.MouseDown

            If e.ChangedButton = MouseButton.Right Then

                CommandBinding_Executed(MoveableImage, Nothing)

            End If

        End Sub

     

    The code in that event handler simply calls the execution code for the original Command (so at least we retain some semblance of coordination).

    Finally, the markup for the Image in the XAML Pane includes a pointer to the new MouseDown event handler:

     

          <Image x:Name="MoveableImage" Width="55" Source="questionmark2.jpg"

           Canvas.Left="0" Canvas.Top="0"

           MouseDown="MoveableImage_MouseDown"

             />

     

    To avoid confusion, I'll show all the revised markup below, as all references to the Command now have '2' appended to them, and they are easy to miss!

     

    <Window x:Class="Window4"

      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       xmlns:local="clr-namespace:GetValue"

       Title="Right Mouse Test" Height="300" Width="300">

     

      <Window.CommandBindings>

        <CommandBinding Command="local:MoveItCommand2.MoveIt"

                       Executed="CommandBinding_Executed"

                       CanExecute="CommandBinding_CanExecute"/>

      </Window.CommandBindings>

     

      <Grid x:Name="MainGrid">

        <Grid.RowDefinitions>

          <RowDefinition Height="Auto" />

          <RowDefinition Height="218*" />

          <RowDefinition Height="Auto" />

        </Grid.RowDefinitions>

     

        <!--  Canvas in the main middle section -->

        <Canvas x:Name="MainCanvas" Grid.Row="1"

            >

          <Image x:Name="MoveableImage" Width="55"

           Source="questionmark2.jpg"

           Canvas.Left="0" Canvas.Top="0"

           MouseDown="MoveableImage_MouseDown" />

        </Canvas>

     

        <!-- Menu at the top -->

        <Menu Grid.Row="0" Margin="3">

          <MenuItem Header="Move It" Margin="5"

            Command="local:MoveItCommand2.MoveIt" />

        </Menu>

     

        <ToolBar Grid.Row="2" >

          <Button Content="Move It"

           Command="local:MoveItCommand2.MoveIt"  />

        </ToolBar>

     

        <Button Grid.Row="1" VerticalAlignment="Bottom"

                 HorizontalAlignment="Right" Width="100"

                 Height="33" Content="Move It" Margin="0,0,4,2"

                 Command="local:MoveItCommand2.MoveIt" />

      </Grid>

    </Window>

     

     

  • How To Create and Use a Custom Command in WPF

      I've got to be honest and admit up front that initially I wasn't completely sold on the idea that WPF Commands are the great leap forward that they are sometimes billed as. OK, so I get that they can reduce the repetition of event handling code, but even their most ardent supporters aren't going to be able to claim that the required code is particularly intuitive. I will agree though that the availability of CanExecute is useful sometimes when multiple controls are bound to a Command. As a WinForms developer moving to WPF, I still find that in many cases I'm happy to use event handlers and, where necessary, a utility method or two.

    So, having got that caveat out of the way, let's look at a basic situation where we want a range of different user actions to trigger the same task.

    I've decided to use the scenario that I covered in this blog. It's maybe not the most realistic scenario, but it does give an opportunity to use a wide range of user inputs and gestures to move the image across the screen. It also allows the use of the CanExecute event.

    In the earlier blog, the user had to click on an image to make it move from left to right across a Canvas. This can soon become tiresome, so by means of a Command we'll give her several other options. In order to make the use of CanExecute realistic, we will say that once the image reaches the far right edge of the Canvas, it must stay there. So the image move will be allowed to happen (i.e. CanExecute) as long as it hasn't reached the right hand edge of the canvas.

    Creating the User Interface
    The following XAML will create an updated version of the Canvas-and-Image UI used previously. This version includes various controls that will be bound to the Command. 

    <Window x:Class="Window3"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

     

       Title="Using A Command" Height="300" Width="300">

     

     

        <Grid x:Name="MainGrid">

        <Grid.RowDefinitions>

          <RowDefinition Height="Auto" />

          <RowDefinition Height="218*" />

          <RowDefinition Height="Auto" />

        </Grid.RowDefinitions>

     

     

          <!--  Canvas in the main middle section -->

        <Canvas x:Name="MainCanvas" Grid.Row="1">

          <Image x:Name="MoveableImage" Width="55" Source="questionmark2.jpg"

     

            Canvas.Left="0" Canvas.Top="0" />

        </Canvas>

     

        <!-- Menu at the top -->

        <Menu Grid.Row="0" Margin="3">

          <MenuItem Header="Move It" Margin="5"

     

                   ></MenuItem>

        </Menu>

     

        <ToolBar Grid.Row="2" >

          <Button Content="Move It"

                  />

        </ToolBar>

     

          <Button Grid.Row="1" VerticalAlignment="Bottom"

                 HorizontalAlignment="Right" Width="100"

                 Height="33" Content="Move It" Margin="0,0,4,2"

                  />

      </Grid>

    </Window>

     (If you've looked at the markup closely and think that the order of elements is haphazard, you would be wrong.   Try moving the MenuItem to what seems a more logical position above the Canvas and you will get an Error when the code is completed.  As you will see, the code behind for the MenuItem will eventually have a link to the Image.   So therefore the Image must be created first in the top down XAML file, before the MenuItem can know about it .  I often think that it's these little potential Gotchas that make the WinForms-to-WPF learning curve so difficult.)

    This is what it should look like:

      

    Creating the Command
    Let's start by creating a new Command Class called MoveItCommand. It will have a single property named MoveIt, a backing field named _move it and a parameterless constructor. The property and field are of type RoutedUICommand, but apart from that are standard and really don't need any additional explanation: 

        Private Shared _moveit As RoutedUICommand

        Public Shared ReadOnly Property MoveIt() As RoutedUICommand

            Get

                Return _moveit

            End Get

        End Property

     The constructor is used to create the various keyboard and mouse gestures that can be used to move the image. For the purposes of demonstration, I've gone totally overboard on these and have included four variations in the final version. But for clarity, I am only showing one in the code snippet below:

         Shared Sub New()

            '  Add keyboard and mouse gestures

            Dim UserInputs As New InputGestureCollection()

            UserInputs.Add(New KeyGesture(Key.M, ModifierKeys.Alt))

     

             '  Assign these gestures to the _moveit field (and thereby to the MoveIt property)

            _moveit = New RoutedUICommand("Move Element", "Move", GetType(MoveItCommand), UserInputs)

     

        End Sub 

    • The constructor begins by creating a new empty collection of mouse gestures.
    • The second line creates and stores a gesture which takes the Alt and M keys as the combination to be used to fire this command. Notice the order of the keys used as arguments - effectively it is "M & Alt" which isn't the way we usually think of the combination. If you switch the order and place the Modifier key first, you will get a runtime error. Trust me, I've been there, done that!
    • Finally, a new RoutedCommand is created, containing descriptive text, its name, owner type (which is this custom command class - MoveItCommand), and the collection of Gestures that will work with this command.

     Note that all the members of this class are Shared, ensuring that only instance of the Command will be in use in the application when it runs.

    Additional Gestures
    If you are happy with how the Class and its Constructor works, I'll now add in those other gestures I mentioned. These include F12 on its own, a mouse click on its own and finally a combination of mouse click and key press. Here's the revised code:  

        Shared Sub New()

            '  Add keyboard and mouse gestures

            Dim UserInputs As New InputGestureCollection()

            UserInputs.Add(New KeyGesture(Key.M, ModifierKeys.Alt))

            UserInputs.Add(New KeyGesture(Key.F12, ModifierKeys.None))

            UserInputs.Add(New MouseGesture(MouseAction.RightClick, ModifierKeys.None))

            UserInputs.Add(New MouseGesture(MouseAction.LeftClick, ModifierKeys.Shift))

             '  Assign these gestures to the _moveit field (and thereby to the MoveIt property)

            _moveit = New RoutedUICommand("Move Element", "Move", GetType(MoveItCommand), UserInputs)

        End Sub

     

    • The first gesture is Alt and M, which I've already covered.
    • The second gesture is the F12 key on its own. Note that you must include Modifiers.None. if you don't, you will get a runtime error.
    • The third gesture will fire the command when the mouse is right clicked anywhere in the Window. Because of the way that WPF and RoutedCommands work, this effectively means that you can still click on the Image to make it move. If you wanted to limit the mouse click to the Image only, you could harness the power of Bubbling and Tunneling events to trap the mouse button press at the Image level. I haven't done that in this example.
    • The final gesture takes a combination of both the mouse left button click together with holding down the Shift key. Quite awkward to use and only included to demonstrate the range of available gestures.

     

    Command Bindings
    Having created the custom Command Class, if we want to use it in the XAML - and we do - then it is necessary to create an XML namespace mapping. Without this link, the XAML markup would have no idea about the existence of the class and its property. The syntax is fairly simple: 

       xmlns:local="clr-namespace:GetValue" 

    The alias of 'local' is used as the key which is mapped to the namespace in which the MoveItCommand class resides. The namespace is the same as the name of the project and in this case the project is named 'GetValue'.

    Given this namespace mapping, we can now set up the CommandBinding in the XAML pane:

     

      <Window.CommandBindings>

        <CommandBinding Command="local:MoveItCommand.MoveIt"

                       Executed="CommandBinding_Executed"

                       CanExecute="CommandBinding_CanExecute"/>

      </Window.CommandBindings>

     

    As you see from the snippet above, you create a new CommandBinding inside a CommandBindings collection. The Command property of the CommandBinding points to that MoveIt property created in the MoveItCommand class.

    The Executed and CanExecute properties point to two methods which we will create next in the code-behind.

    Executing The CommandBinding The task of the CommandBinding_Executed method is to carry out whatever tasks we want to have actioned when the command is invoked. In this demonstration project, this task is simply to shunt the image a few units to the right. So the code is as follows:  

        Private Sub CommandBinding_Executed(ByVal sender As System.Object, ByVal e As System.Windows.Input.ExecutedRoutedEventArgs)

            '  Move the image 5 units to the right

            Dim LeftPos As Double = MoveableImage.GetValue(Canvas.LeftProperty)

            MoveableImage.SetValue(Canvas.LeftProperty, LeftPos + 5)

     

        End Sub 

    (If you are not sure about how the GetValue and SetValue functions work, you can read up on it in my blog here).

    CanExecute
    CanExecute is an optional feature which can be quite useful. In this demonstration, we will only allow the Command to work (and therefore only allow the Image to continue moving to the right) if it hasn't reached the right hand edge of the Canvas. More subtly, if CanExecute becomes set to False, any controls that are bound to the Command will automatically be disabled. I will add Command Bindings to the buttons, etc shortly.

    First, here is the code for the CanExecute method: 

        Private Sub CommandBinding_CanExecute(ByVal sender As System.Object, ByVal e As System.Windows.Input.CanExecuteRoutedEventArgs)

            '  Only allow execution of the command if the image

            '  has not yet reached the right hand edge.

            e.CanExecute = MoveableImage.GetValue(Canvas.LeftProperty) < (MainCanvas.ActualWidth - MoveableImage.Width)

     

        End Sub 

    It tests the current position of the Image to see if it has reached the right hand edge of the Canvas. CanExecute will be set to True if there is still space to the right side of the Image, otherwise it will be set to False.

    The complete code behind for the Window and the Custom Command class together now looks like this: 

    Partial Public Class Window3

     

        Private Sub CommandBinding_Executed(ByVal sender As System.Object, ByVal e As System.Windows.Input.ExecutedRoutedEventArgs)

            '  Move the image 5 units to the right

            Dim LeftPos As Double = MoveableImage.GetValue(Canvas.LeftProperty)

            MoveableImage.SetValue(Canvas.LeftProperty, LeftPos + 5)

     

        End Sub

     

        Private Sub CommandBinding_CanExecute(ByVal sender As System.Object, ByVal e As System.Windows.Input.CanExecuteRoutedEventArgs)

            '  Only allow execution of the command if the image

            '  has not yet reached the right hand edge.

            e.CanExecute = MoveableImage.GetValue(Canvas.LeftProperty) < (MainCanvas.ActualWidth - MoveableImage.Width)

     

        End Sub

    End Class

     

    Public Class MoveItCommand

     

        Private Shared _moveit As RoutedUICommand

        Public Shared ReadOnly Property MoveIt() As RoutedUICommand

            Get

                Return _moveit

            End Get

        End Property

     

        Shared Sub New()

            '  Add keyboard and mouse gestures

            Dim UserInputs As New InputGestureCollection()

            UserInputs.Add(New KeyGesture(Key.M, ModifierKeys.Alt))

            UserInputs.Add(New KeyGesture(Key.F12, ModifierKeys.None))

            UserInputs.Add(New MouseGesture(MouseAction.RightClick, ModifierKeys.None))

            UserInputs.Add(New MouseGesture(MouseAction.LeftClick, ModifierKeys.Shift))

             '  Assign these gestures to the _moveit field (and thereby to the MoveIt property)

            _moveit = New RoutedUICommand("Move Element", "Move", GetType(MoveItCommand), UserInputs)

        End Sub

     

    End Class 

    At this point, you can run the project and use any of those four gestures to move the Image. Once the Image hits the right hand edge, it will stop moving. The menu item, the two buttons don't play any part in the action yet - that is, clicking them will have no effect and they won't become disabled when the Image reaches the right hand limit.

    Binding the Command to the Controls
    All that remains to do is to bind this custom command to the menu item and the two buttons. Now things really do become simple and you can begin to see the potential benefit of using a Command. The following piece of markup is added to the MenuItem, the Button in the Toolbar and the Button in the Canvas:  

            Command="local:MoveItCommand.MoveIt" 

    With those bindings in place, you can move the Image by any of the seven methods included - the four input gestures and by clicking on any of the three controls. Note now that once the Image hits the right hand edge, those three controls all become disabled.

    So, there is a fairly simple example of using your own Command. You can see the final version of the XAML markup here, and also the code behind here.

  • How To Edit Items in a Windows Forms ListView

    As I was finishing off the previous blog item on the Windows Forms ListView, it occurred to me that there isn't much documentation around to explain how to edit the ListView items.   So I thought it might be useful to cover a couple of approaches in this blog.

    If you're disappointed that there isn't a built-in way to edit all the items and sub items, the thing to bear in mind is that ListView is essentially a display control, rather than an editing one. That said, there is one built-in tool that you can use - but only as long as you are content to edit items in the first column exclusively.

    Edit First Column Only
    What you can do is set the ListView's LabelEdit property to True. You can set this via the Properties Window or in code, ideally in the Load Event:

     

        Private Sub Form2_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

            LVMVPs.LabelEdit = True

        End Sub

     

    Now, if you click on an item in the first column on any row, the data in the first column of the selected row will become editable. I actually found that I had to triple-click, but I'm not sure if this is just a reflection of my mouse click speed settings or is the default requirement:

     

      

     

    If you have the FullRowSelect property set to True, you can click anywhere along the row and the item in the first column will become editable as shown in the screenshot above. If FullRowSelect is set to False then you do need to click directly on the first column.

     

      

     

    Edit Any Column
    You have to do a bit of work yourself if you want to be able to edit any cell in the ListView, but there isn't really much to it. To demonstrate the technique, I've added three TextBoxes and an Update button to the Form:

     

      

    The code below will allow the user to Right-click on any row and this will cause the three items of data in that row to be displayed in the three TextBoxes.

        Dim ItemSelected As ListViewItem

       Private Sub LVMVPs_MouseDown(ByVal sender As Object, _

        ByVal e As MouseEventArgs) Handles LVMVPs.MouseDown

            '  Test that user wants to edit

            If e.Button = Windows.Forms.MouseButtons.Right Then

                '  Identify the selected ListViewItem

                ItemSelected = LVMVPs.GetItemAt(e.X, e.Y)

     

                ' Display in TextBoxes to allow editing

                If (ItemSelected IsNot Nothing) Then

                    TextBox1.Text = (ItemSelected.SubItems(0).Text)

                    TextBox2.Text = (ItemSelected.SubItems(1).Text)

                    TextBox3.Text = (ItemSelected.SubItems(2).Text)

                End If

            End If

        End Sub

     

    As you can see, it first tests that the Right mouse button has been clicked and if it has then each of the sub items is pasted into an individual TextBox below. The key feature in this block of code is the GetItemAt method of the ListView. This is the magic ingredient that allows you to easily identify exactly which row the user wants to deal with (and if you've spent a lot of fruitless time trying to work out how to use other ListView selection methods, you'll be pleased that you landed here!). Note that ItemSelected (the variable I am using to hold the current selection) is a ListViewItem object, not an index number - which is where most people seem to get caught.

    The Confirm code is also very simple. 

        Private Sub btnConfirm_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnConfirm.Click

            '  Update ListView

            ItemSelected.SubItems(0).Text = TextBox1.Text

            ItemSelected.SubItems(1).Text = TextBox2.Text

            ItemSelected.SubItems(2).Text = TextBox3.Text

            '  Avoid confusion by clearing TextBoxes

            TextBox1.Clear()

            TextBox2.Clear()

            TextBox3.Clear()

     

        End Sub

     

    The content of each TextBox is transferred back to the relevant ListView sub item (regardless of whether it has been changed - although you could of course build in a filter here if you wanted to). The TextBoxes are then cleared.

    And for a simple project, that's really all there is to it. As you can see, I did also add a button to clear the ListView, but that was just for my own convenience, so I could test multiple consecutive edits without duplicating the data in the file. 

        Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

            LVMVPs.Clear()

        End Sub

     

    Not so obviously, I did also make a change to the file save code. I wanted to ensure that there wasn't a new blank line at the end of the text file, which might have caused a problem, so I tweaked the btnSave Click event as shown below: 

        Private Sub btnSave_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSave.Click

            '  Create FileStream and StreamWriter to access and write to file

            Dim FS As New FileStream(TextFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)

            Dim SW As New StreamWriter(FS)

     

            Try

                '  Loop through all rows

                For Index As Integer = 0 To LVMVPs.Items.Count - 1

                    '  Internally loop through all columns, gathering items

                    For SubIndex As Integer = 0 To LVMVPs.Items(Index).SubItems.Count - 1

                        SW.Write(LVMVPs.Items(Index).SubItems(SubIndex).Text & Chr(9))

                    Next

                    '  Create new line ready for next row

                    '  except for final entry

                    If Index = LVMVPs.Items.Count - 1 Then

                        Exit For

                    Else

                        SW.Write(Environment.NewLine)

                    End If

     

                Next

            Catch ex As Exception

     

                MsgBox("Error saving to file." & ex.Message)

     

            Finally

     

                SW.Close()

                FS.Close()

     

            End Try

     

        End Sub

     

    To avoid errors when there is no data in the TextBoxes, you might also want to disable and enable the Confirm button when appropriate. The sample project which you can download from here, includes this feature. 

  • WPF: How To Move Controls and Elements At Runtime

     XAML is brilliant for creating user interfaces at design time, but if you need to make changes dynamically at runtime, this can sometimes be a problem. To take an example, let's say that you need to allow an Image to move to a different location on screen in response to some user action.

    To demonstrate this, we can use an Image which is housed in a Canvas. So the first step is to create a Canvas in a WPF Window, place the Image inside the Canvas and assign a value to the Source property of the Image:  

    <Window x:Class="Window2"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       Title="Image In A Canvas" Height="300" Width="300">

      <Canvas x:Name="MainCanvas">

        <Image x:Name="MoveableImage" Width="55" Source="questionmark2.jpg" />

      </Canvas>

    </Window>

     We can then have the Image move a few units to the right each time it receives a mouse click. The WPF Image doesn't support the Click event, but MouseDown is available for the same purpose. So the next step is to wire up an event handler for this:

        <Image x:Name="MoveableImage" Width="55" Source="questionmark2.jpg"

            MouseDown="Image_MouseDown" />

    Here's the first pass at the Image_MouseDown event in the code-behind to move the Image 10 units to the right on each mouse down:

        Private Sub Image_MouseDown(ByVal sender As System.Object, ByVal e As System.Windows.Input.MouseButtonEventArgs)

            Dim LeftPos As Double = MoveableImage.GetValue(Canvas.LeftProperty)

     

            MoveableImage.SetValue(Canvas.LeftProperty, LeftPos + 10)

     

        End Sub

    This seems straightforward enough, doesn't it? The variable named LeftPos reads the value that is currently stored as the attached Canvas.Left property of the Image, then it increments that value by 10. If you were to run this project, you would expect that the Image would jump 10 units to the right when the mouse is clicked on it. But if you try this code, you will find that nothing happens when the Image is clicked.

    There's a little WPF Gotcha lurking in the canvas, just waiting to confuse you. When you create this layout, as you can see from the screenshot below, the Image appears in the top left hand corner of the Canvas by default:

     

          

    You would be forgiven for assuming that the attached Canvas.Left and Canvas.Top values of the Image are set to zero. It's a reasonable assumption to make, especially as you can see the Image in exactly that location. However, it doesn't actually work like that. By default the Left and Top attached properties are in fact set to Double.NaN - Not A Number. This is effectively a null value that is assigned in order to allow the Canvas to make use of its own built-in layout intelligence for more complex scenarios.

    The fix is simple - you just have to assign values to the Canvas.Left and Canvas.Top properties in the XAML when you create the Image element, but it's something that isn't obvious. Not only does it mean that the expected movement of the Image doesn't happen, but it will actually cause your application to crash if you try and reset the value in code to move the Image back to the start position (which we will do later).

    So, here's the amended XAML for the Image: 

        <Image x:Name="MoveableImage" Width="55" Source="questionmark2.jpg"

            MouseDown="Image_MouseDown"

            Canvas.Left="0" Canvas.Top="0" /> 

    With that change in place, the Image will now move when it is clicked.

    In this kind of situation, you probably won't want the Image to either get stuck at the right hand side of the Canvas or, possibly even worse, move out of sight. A simple If/Then statement will fix this: 

            If LeftPos < (MainCanvas.ActualWidth - MoveableImage.Width) Then

                MoveableImage.SetValue(Canvas.LeftProperty, LeftPos + 10)

            Else

                MoveableImage.SetValue(Canvas.LeftProperty, CDbl(0))

            End If 

    As you can see, the first line tests if there is still enough width remaining for the Image to be moved 10 elements to the right and still be visible. If there is, then the 10 unit jump takes place. If not, then the Image is shunted back to far left of the Canvas. Note that this will fail if you haven't set the initial value of the Canvas.Left property as described earlier.

    It will also fail if you don't cast the value (in this example, zero) to a Double. This is because the second parameter of the SetValue method is a generic Object Type and the Canvas.Left property requires a Double.

    Moving the Image downwards follows the same kind of logic. You set an initial value on the Canvas.Top property then increment this when the MouseDown event fires. Perhaps you want to build in a feature where the left mouse button causes the Image to move left and the right mouse button to move it down.

    If you are moving from Windows Forms to WPF, you may expect to find the Button property of the MouseEventArgs. You would then use code along the lines of:

            If e.Button = Windows.Forms.MouseButtons.Left Then

                ' Do Something

            End If

    WPF is (again) slightly different. It uses the System.Windows.Input.MouseButtonEventArgs class in place of the Windows Forms version and it has a ChangedButton property, not a Button property. So the WPF version of the previous snippet will start with: 

     If e.ChangedButton = MouseButton.Left Then 

    The following code snippet combines the use of either the left or right mouse button to move the Image left or down respectively:

        Private Sub Image_MouseDown(ByVal sender As System.Object, ByVal e As System.Windows.Input.MouseButtonEventArgs)

     

            If e.ChangedButton = MouseButton.Left Then

     

                Dim LeftPos As Double = MoveableImage.GetValue(Canvas.LeftProperty)

     

                If LeftPos < (MainCanvas.ActualWidth - MoveableImage.Width) Then

                    MoveableImage.SetValue(Canvas.LeftProperty, LeftPos + 10)

                Else

                    MoveableImage.SetValue(Canvas.LeftProperty, CDbl(0))

                End If

     

     

            ElseIf e.ChangedButton = MouseButton.Right Then

                Dim TopPos As Double = MoveableImage.GetValue(Canvas.TopProperty)

     

                If TopPos < (MainCanvas.ActualHeight - MoveableImage.Width) Then

                    MoveableImage.SetValue(Canvas.TopProperty, TopPos + 10)

                Else

                    MoveableImage.SetValue(Canvas.TopProperty, CDbl(0))

                End If

            End If

     

        End Sub

    Finally, of course, you can combine the horizontal and vertical movement of the Image - maybe by using the Middle mouse button:

            ElseIf e.ChangedButton = MouseButton.Middle Then

                MoveableImage.SetValue(Canvas.LeftProperty, MoveableImage.GetValue(Canvas.LeftProperty) + 10)

                MoveableImage.SetValue(Canvas.TopProperty, MoveableImage.GetValue(Canvas.TopProperty) + 10)

            End If

    The above snippet can be expanded to have the tests for reaching the outer edges built in also.

    The main issues I particularly wanted to cover in this blog item were the GetValue and SetValue methods, which are a very useful tool in these kind of situations. I also wanted to flag up the various WPF Gotchas that might trip up the unwary WinForms developer. In a follow-up blog, I plan to take this task a step further and look at ways of giving the user several ways of achieving the Image movement - all of which loop back to a single piece of code

  • Populating a WinForms ListView from a Text File

     The Windows Forms ListView is designed to display data. You can use a simple text file as the data source of a Windows Forms ListView. Let's start with a scenario where you have a txt file that contains the data. Each line of the text file represents one row of data to be displayed in the ListView. The content for each column item (or cell) is delimited by the use of a TAB character.

    A simple text file along these lines might contain the following entries:

    Ged Mead UK
    Serge Baranovsky    USA
    Larry Blake USA
    Scott Waletzko      USA
    Mark Dryden UK
    Dave Jeavons UK
    Chris Manning USA

    (The uneven layout is caused by the use of the TAB as the delimiter)

    Reading data from a file
    An easy way of transferring this data from the file to the ListView is to:

    • Use a StreamReader which accesses the text file and then reads it line by line.
    • As each new line is read, it is split by means of the TAB character and temporarily stored in a String array.
    • Next, the first element is pulled out of the String array and used as the item for the first column of the ListView (the ListViewItem).
    • Then the remaining two elements are pulled out of the String array in turn and assigned as the SubItems of the ListViewItem.

     

    Here's the code that carries out those steps:

      ' Variable for file to hold data

        Dim TextFile As String = "F:\MVPs.txt"

     

        Private Sub btnGet_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGet.Click

     

            Try

                ' Declare StreamReader and pass the Path of the text file to be read as a Parameter

                Dim SR As New StreamReader(TextFile)

                '  Variable to hold data as it is read line by line

                Dim strTemp() As String

     

                Do While SR.Peek <> -1 ' Use Peek to read the file until there are no more lines

                    '  Create a variable for the ListViewItems

                    Dim LVItem As New ListViewItem

                    '  Read the next line in file and Split it using the TAB.

                    strTemp = SR.ReadLine.Split(Chr(9))

                    '  Pull out the first element in the line and assign it as

                    '  the data for the first column.

                    LVItem.Text = strTemp(0).ToString

                    '  Add the item to the ListView

                    LVMVPs.Items.Add(LVItem)

                    '  Assign elements 2 & 3 as the subitems.

                    LVItem.SubItems.Add(strTemp(1).ToString)

                    LVItem.SubItems.Add(strTemp(2).ToString)

                Loop

     

                SR.Close() ' Close the StreamReader

     

            Catch ex As Exception

     

                MsgBox("Error reading file." & ex.Message)

     

            End Try

     

        End Sub

    You'll see that I've used a variable to hold the path to the file that contains the data. You will also need to include an Imports statement for System.IO at the top of the Class file.

    Although the above code will successfully access the file, read and split the data and then populate the ListView, the result you will see may not be what you expect or want:

      

    The first problem is that the View Property of the ListView needs to be set to Details. By default it is set to LargeIcon, with the result you see in the screenshot. You can make this change via the Properties Window or in code. 

            LVMVPs.View = View.Details 

    Make that change, run the project, hit the 'Get From File' button and you will see:

      

    Again, definitely not what you want! The problem here is that the ListView won't automatically create columns for you, even though you might expect that it would based on the data you are feeding in. As it is traditional to have some header text at the top of each column, you can create this at the same time as you create the column itself .  

            LVMVPs.Columns.Add("First Name", 76)

            LVMVPs.Columns.Add("Surname", 96)

            LVMVPs.Columns.Add("Location", 56) 

     

    Now, when you run the project again you will see the data neatly tabulated in the ListView.

      

    I have also set the width property of each column in the code above, because the default width would result in some of the longer strings being truncated otherwise.

    If you don't want to hard code those column headers, you can change the text file structure and have the column headers written to the first line of the file. You would then pull these out first and assign them to the columns, before using the Do While loop to read and display the rest of the file.

    Writing data to a file
    Although a ListView's key purpose is to display existing data, there may be times when you have edited the content and want to save it back to a text file. The steps involved in doing this are as follows:

    • Use a FileStream to set up the file you are going to write to.
    • Use a StreamWriter to access the chosen file.
    • Loop through each row of the ListView, pull the text data from each column in turn and write it to the text file, adding a TAB character after each item.
    • Add a new line at the end of each loop.  

    The code for this process is:

       Private Sub btnSave_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSave.Click

            '  Create FileStream and StreamWriter to access and write to file

            Dim FS As New FileStream(TextFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)

            Dim SW As New StreamWriter(FS)

     

            Try

                '  Loop through all rows

                For Index As Integer = 0 To LVMVPs.Items.Count - 1

                    '  Internally loop through all columns, gathering items

                    For SubIndex As Integer = 0 To LVMVPs.Items(Index).SubItems.Count - 1

                        SW.Write(LVMVPs.Items(Index).SubItems(SubIndex).Text & Chr(9))

                    Next

                    '  Create new line ready for next row

                    SW.Write(Environment.NewLine)

     

                Next

            Catch ex As Exception

     

                MsgBox("Error saving to file." & ex.Message)

     

            Finally

     

                SW.Close()

                FS.Close()

     

            End Try

     

        End Sub

    For many simple scenarios where you want to display tabulated text in a ListView (and optionally save it back to a file), the above approaches will work just fine. There will of course be times when you need something more sophisticated and I will look at some of those in later blogs.

  • Sorting and Grouping Items in a WPF ListBox - Part 2

     

    Introduction
    In the first part, I outlined the sample application and said that it:

    • will create a collection of Person objects and databind them to a ListBox.
    • will use very simple DataTemplates to format two properties of the Person class - FullName and Status.
    • will use a GroupStyle HeaderTemplate to display a third property of the Person class - Category.
    • groups the Person objects by their Category property.
    • sorts the Categories and displays them in alphabetical order.
    • sorts the Persons by name inside the Category groups and displays them in alphabetical order.  

    The first five tasks are all covered in that previous part. You can download the project up to this point from here.

    Sorting the Categories
    In the same way that I used a Group Description to group items together, WPF offers the SortDescription to allow data inside the CollectionView to be sorted. The following code in the Window Loaded event is all that's needed:

            currentView.SortDescriptions.Add(New SortDescription("Category", ListSortDirection.Ascending))

    The SortDescription takes two parameters. The first is the name of the property that you want to sort on. The second is one of two choices for the sort direction.

    For completeness, this is the full code so far in the Window Loaded event (which creates the DataContext, the View, the GroupDescription and the SortDescription):

        Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

            Contacts = Person.GetPersons

            Me.DataContext = Contacts

     

            Dim currentView As ICollectionView

            currentView = CollectionViewSource.GetDefaultView(Contacts)

     

            '  Group

            currentView.GroupDescriptions.Add(New PropertyGroupDescription("Category"))

     

            '   Sort Categories

            currentView.SortDescriptions.Add(New SortDescription("Category", ListSortDirection.Ascending))

     

        End Sub

    The sorted Categories will look like this:

     

    Sorting the ListItems
    As you can see, the Categories are now in alphabetical order. The names of the Person objects however are still shown in the relative order in which they were created. To create this secondary sort, the same kind of code is used:

            currentView.SortDescriptions.Add(New SortDescription("FullName", ListSortDirection.Ascending))

    The resulting display is:

      

    The clearest example is where you can see the three items in the 'Work Colleagues' Category. They are now sorted alphabetically.

    Note that the order that you list the sorting tasks is important. If you were to reverse the order:

           '   Sort individual names

            currentView.SortDescriptions.Add(New SortDescription("FullName", ListSortDirection.Ascending))

     

            '   Sort Categories

            currentView.SortDescriptions.Add(New SortDescription("Category", ListSortDirection.Ascending))

     

    You would effectively lose the Category sort.

      

    It is possible to create and apply the grouping and sorting in XAML, but I always try as far as possible to handle these kind of tasks in code-behind. Equally, I try and use XAML for the actual graphical display. There isn't a clear line between the two sometimes, of course, and you have to allow for individual preferences, but I try and keep to this approach as a general rule.

  • How To Sort and Group ListItems in a WPF ListBox

    Introduction
    I was going to title this blog "What's in a name?" because William Shakespeare's famous question smacked me on the head recently after what seemed like several hours of frustration. The answer in this particular case is "Quite a lot!". As you'll see when I cover the syntax used to group items, you can very easily fall into a trap when it comes to names.

    But first, I'll need to set the scene. This small application:

    • will create a collection of Person objects and databind them to a ListBox.
    • will use very simple DataTemplates to format two properties of the Person class - FullName and Status.
    • will use a GroupStyle HeaderTemplate to display a third property of the Person class - Category.
    • groups the Person objects by their Category property.
    • sorts the Categories and displays them in alphabetical order.
    • sorts the Persons by name inside the Category groups and displays them in alphabetical order.

     The Person Class
    First, the Person Class - which I have chosen to implement INotifyPropertyChanged, although I don't actually take advantage of the change notification in this simple example:

    Imports System.ComponentModel

    Imports System.Collections.ObjectModel

     

    Public Class Person

        Implements INotifyPropertyChanged

     

        Sub New(ByVal personname As String, ByVal personstatus As String, ByVal personsgroup As String)

            Me.FullName = personname

            Me.Status = personstatus

            Me.Category = personsgroup

        End Sub

     

        Private _name As String

        Public Property FullName() As String

            Get

                Return _name

            End Get

            Set(ByVal value As String)

                _name = value

                OnPropertyChanged(New PropertyChangedEventArgs("FullName"))

            End Set

        End Property

     

        Private _status As String

        Public Property Status() As String

            Get

                Return _status

            End Get

            Set(ByVal value As String)

                _status = value

                OnPropertyChanged(New PropertyChangedEventArgs("Status"))

            End Set

        End Property

     

        Private _Category As String

        Public Property Category() As String

            Get

                Return _Category

            End Get

            Set(ByVal value As String)

                _Category = value

                OnPropertyChanged(New PropertyChangedEventArgs("Category"))

            End Set

        End Property

     

        Public Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs)

            If Not PropertyChangedEvent Is Nothing Then

                RaiseEvent PropertyChanged(Me, e)

            End If

        End Sub 

     

        Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

     

     

        Public Shared Function GetPersons() As List(Of Person)

            Dim GP As New List(Of Person)

            GP.Add(New Person("Neil Birch", "Available", "My Friends"))

            GP.Add(New Person("Joe Brown", "On Site", "Work Colleagues"))

            GP.Add(New Person("Larry Blake", "Available", "VB City"))

            GP.Add(New Person("Fran Mead", "At Work", "Family"))

            GP.Add(New Person("Elaine Javan", "On Vacation", "Work Colleagues"))

            GP.Add(New Person("Matt Higginbotham", "On Line", "VB City"))

            GP.Add(New Person("Zoe Flint", "On Site", "Work Colleagues"))

            Return GP

        End Function

     

    End Class

    Essentially, all you need to note for the purpose of this article is that the GetPersons function creates a List (of Person) and each Person instance has values assigned to all three of the properties of the class - FullName, Status, and Category.

    The WPF Window
    The WPF Application contains just one Window. A List of Persons is created by using the GetPersons function and this List is used as the DataContext for the Window. This will allow the ListBox to access that List. The initial code-behind is as follows:

    Class Window1

        Dim Contacts As New List(Of Person)

        Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

            Contacts = Person.GetPersons

            Me.DataContext = Contacts

        End Sub

     

    End Class

    The ListBox
    In the XAML for Window1, there is a ListBox. To begin with, this ListBox simply shows the Person's FullName, followed by their Status. There is a minimal amount of formatting in the two TextBlocks that are used for this.

          <ListBox Name="lstContacts"

            ItemsSource="{Binding}"

            Margin="6,6,3,3" >

     

     

            <ListBox.ItemTemplate>

            <DataTemplate >

              <StackPanel >

                <TextBlock Text="{Binding Path=FullName}"

                  Margin="0,2,0,0"/>

                <TextBlock Text="{Binding Path=Status}"

                  Margin="6,0,0,0" FontSize="11" Foreground="Navy" />

     

              </StackPanel>

            </DataTemplate>

          </ListBox.ItemTemplate>

        </ListBox>

    The result so far is this:

    Obviously, there is no sorting or grouping going on there yet.

    CollectionView
    When you set up the Binding between the data source (the List of Persons) and the target control (the ListBox), a CollectionView is created automatically. This is a wrapper for the binding and allows you to sort, filter, group or navigate through the collection without affecting the underlying collection itself. Think of it as an editable snapshot of the data and you won't be far off the mark

    You can access the current view by using the GetDefaultView method and passing in the name of the data source - in this case, the Contacts List created in the code-behind of Window1. In our example, we will access the CollectionView and then group and sort the items as they are displayed in the ListBox. To try and keep things as straightforward as possible I'll tackle each of these one at a time.

    Creation of an instance of the view by means of the GetDefaultView method is simple.  

    Imports System.ComponentModel

     

        Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

            Contacts = Person.GetPersons

            Me.DataContext = Contacts

     

            Dim currentView As ICollectionView

            currentView = CollectionViewSource.GetDefaultView(Contacts)

        End Sub

     

    End Class

    Note the inclusion of the Imports statement for System.ComponentModel, the class which houses ICollectionView.   

    It's the final two lines of the Window_Loaded event that create the View:

       

    Grouping

    The following line of code, placed in the Window_Loaded event, will group the individual items based on the Category property:

            currentView.GroupDescriptions.Add(New PropertyGroupDescription("Category"))

    The result is shown below:

      

    So I don't have to be much of a mind reader to know that you're not too impressed at this point. In fact, you probably don't even believe that the items are now grouped. And even if they are, it's not very obvious to the user.

    Let's start with the question of whether they are really grouped.   If you look carefully at the order of the names you will see that they have changed from the way they were originally listed - as seen in the earlier screenshot. And if you take a peek at the code which created the Person instances you will be able to see the value of the Category property for each instance.

        Public Shared Function GetPersons() As List(Of Person)

            Dim GP As New List(Of Person)

            GP.Add(New Person("Neil Birch", "Available", "My Friends"))

            GP.Add(New Person("Joe Brown", "On Site", "Work Colleagues"))

            GP.Add(New Person("Larry Blake", "Available", "VB City"))

            GP.Add(New Person("Fran Mead", "At Work", "Family"))

            GP.Add(New Person("Elaine Javan", "On Vacation", "Work Colleagues"))

            GP.Add(New Person("Matt Higginbotham", "On Line", "VB City"))

            GP.Add(New Person("Zoe Flint", "On Site", "Work Colleagues"))

            Return GP

        End Function

    Now, you can see that the first Category is "My Friends" and Neil Birch is the sole member of that Category. More usefully, the next three instances - Joe Brown, Elaine Javan and Zoe Flint all have "Work Colleagues" as their Category. They are now all listed consecutively - and both Elaine Javan and Zoe Flint have been moved from their original positions.

    The next two names - Larry Blake and Matt Higginbotham are both in the "VB City" Category. Fran Mead is the sole member of the "Family" Category.

    So, the grouping has actually taken place. The order of the Categories is based on the order in which they first appear in the GetPersons function.

    Clearly, we need something to make the grouping of Categories more obvious. And that 'something' is a GroupStyle. GroupStyle has a HeaderTemplate property which can be used to format the text and/or graphics that are displayed at the start of each group - in this case at the start of each Category.

    The XAML is a little bit verbose, but - apart from one potential Gotcha - is straightforward.

          <ListBox.GroupStyle>

            <GroupStyle>

              <GroupStyle.HeaderTemplate>

                <DataTemplate>

                  <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>

                </DataTemplate>

              </GroupStyle.HeaderTemplate>

            </GroupStyle>

          </ListBox.GroupStyle>

    The GroupStyle has a HeaderTemplate. The HeaderTemplate contains a DataTemplate. In this case I have chosen to include only a basic TextBlock in the DataTemplate. The Text property of the TextBlock needs to show the Category name.

    This is where I managed to get myself quite confused.

      <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>

    I initially had a property in the Person class called Name. (I've since changed it to FullName for clarity.) I couldn't understand why the Path of a TextBlock that showed the Category value would be pointing to the Name property of Person class. And of course it doesn't. But, while I was getting to grips with this I set the path to what I thought was the most logical item - the Category property. In other words, I had it like this:

     <TextBlock Text="{Binding Path=Category}" FontWeight="Bold"/>

    Now, WPF is so forgiving when it comes to this kind of data binding that it doesn't throw an exception (quite rightly, because it is a valid path to an existing field). Sadly it doesn't show the Categories either. The result at this point is:

      

    You can see that they are grouped, but there's no header.   The key lesson to take away from this is that the Path in that particular binding points to the name that is assigned to the PropertyGroupDescription in the code-behind:

       currentView.GroupDescriptions.Add(New PropertyGroupDescription("Category"))

    Confused yet? Put simply, you always use the syntax of:

                  <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>

    regardless of what the actual name of the grouping property is. You're probably thinking that we're well into the "Too much information" stage now, but - apart from wanting to share my pain - I really think that this is Gotcha that is just waiting to bite the unwary and so it was worth spending a couple of extra minutes looking at it.

    OK, so getting back on track, the correct version of the GroupStyle markup will bring you the result you want. I have included all the ListBox XAML so you can see the finished product:

       <ListBox Name="lstContacts"

            ItemsSource="{Binding}"

            Margin="6,6,3,3" >

     

          <ListBox.GroupStyle>

            <GroupStyle>

              <GroupStyle.HeaderTemplate>

                <DataTemplate>

                  <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>

                </DataTemplate>

              </GroupStyle.HeaderTemplate>

            </GroupStyle>

          </ListBox.GroupStyle>

     

          <ListBox.ItemTemplate>

            <DataTemplate >

              <StackPanel >

                <TextBlock Text="{Binding Path=FullName}"

                  Margin="0,2,0,0"/>

                <TextBlock Text="{Binding Path=Status}"

                  Margin="6,0,0,0" FontSize="11" Foreground="Navy" />

     

              </StackPanel>

            </DataTemplate>

          </ListBox.ItemTemplate>

        </ListBox>

    Here it is:

    The grouping is clear, has a useful header and - more to the point - is accurate. The Categories are not yet in alphabetical order. The individual Person instances are also not sorted within their Categories, as you can see from the order in the Work Colleagues group.

    As this blog has become a bit longer than I expected, I will continue with a Part 2, which will cover the Sorting methods.

    I have posted a copy of this project which you can download from here.

  • WPF ValueConverter Using Enumeration and Image Paths

     

    In the previous blogs on ValueConverters - here and here - the conversion was from Integer type to Brush. In this blog I will cover the situation where you want to include a small image or icon in the ListView.

    There are various alternatives here. You could hard code the image path into the collection, but the problem may be that you don't know the exact image paths at the time the collection is created. The approach I want to use is where you parse the data as it feeds through the binding and you select a stored image from the hard drive, the selection being based on the value of a specific field. This decouples the details of the image from the collection itself, which may be useful in many situations.

    The DrinkProduct class looks like this: 

    Public Class DrinkProduct

        Enum MaterialType

            Granules

            Leaf

            Liquid

            Paste

            Powder

            Other

        End Enum

        Sub New(ByVal ID As String, ByVal Name As String, ByVal PackType As String, _

               ByVal BaseMaterial As MaterialType, ByVal Sales As Integer, ByVal Qty As Integer)

            Me.ProductID = ID

            Me.ProductName = Name

            Me.PackageType = PackType

            Me.Material = BaseMaterial

            Me.AnnualSales = Sales

            Me.Quantity = Qty

        End Sub

     

        Private _ProductID As String

        Public Property ProductID() As String

            Get

                Return _ProductID

            End Get

            Set(ByVal value As String)

                _ProductID = value

            End Set

        End Property

     

        Private _ProductName As String

        Public Property ProductName() As String

            Get

                Return _ProductName

            End Get

            Set(ByVal value As String)

                _ProductName = value

            End Set

        End Property

     

        Private _PackageType As String

        Public Property PackageType() As String

            Get

                Return _PackageType

            End Get

            Set(ByVal value As String)

                _PackageType = value

            End Set

        End Property

     

     

        Private _Material As MaterialType

        Public Property Material() As MaterialType

            Get

                Return _Material

            End Get

            Set(ByVal value As MaterialType)

                _Material = value

            End Set

        End Property

     

        Private _quantity As Integer

        Public Property Quantity() As Integer

            Get

                Return _quantity

            End Get

            Set(ByVal value As Integer)

                _quantity = value

            End Set

        End Property

     

     

        Private _annualsales As Integer

        Public Property AnnualSales() As Integer

            Get

                Return _annualsales

            End Get

            Set(ByVal value As Integer)

                _annualsales = value

            End Set

        End Property

     

        Public Shared Function StockCheck() As List(Of DrinkProduct)

            Dim CurrentProducts As New List(Of DrinkProduct)

            With CurrentProducts

                .Add(New DrinkProduct("CF1kg", "Coffee Powder", "1 Kg", MaterialType.Powder, 15684, 1276))

                .Add(New DrinkProduct("CFB500", "Ground Coffee", "500 g", MaterialType.Powder, 22785, 12856))

                .Add(New DrinkProduct("CFG500", "Coffee Granules", "500 g", MaterialType.Granules, 19233, 5907))

                .Add(New DrinkProduct("Te500", "Tea", "500 g", MaterialType.Leaf, 8544, 235))

                .Add(New DrinkProduct("TeInst500", "Instant Tea", "500 g", MaterialType.Powder, 1009, 22))

                .Add(New DrinkProduct("SMlk1lt", "Skimmed Milk", "1 Litre", MaterialType.Liquid, 28012, 2650))

                .Add(New DrinkProduct("HiJ300", "HiJuice Drink Mix", "300 g", MaterialType.Other, 578, 179))

                .Add(New DrinkProduct("Sm400", "Smoothie", "400ml", MaterialType.Paste, 9346, 3284))

                .Add(New DrinkProduct("Beef300", "Beef Drink", "300 g", MaterialType.Granules, 8316, 1965))

                .Add(New DrinkProduct("Beef750", "Beef Drink", "750 g", MaterialType.Granules, 7612, 359))

     

            End With

     

            Return CurrentProducts

        End Function

     

    End Class 

    The Enumeration named 'MaterialType' identifies whether the product is powder, liquid, granule, etc and this is what I will use as the key for selecting the appropriate image.

    The ValueConverter class is similar to those used in the previous blogs - IValueConverter requires the two methods named Convert and ConvertBack. ConvertBack serves no purpose is this scenario, so only throws a not implemented exception. 

    Public Class MaterialToImagePathConverter

        Implements IValueConverter

     

        Public Function Convert(ByVal value As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert

            Select Case value.ToString

                Case "Powder"

                    Return "Images/powderbrown.jpg"

                Case "Liquid"

                    Return "Images/liquiddrop4.jpg"

                Case "Leaf"

                    Return "Images/leaf.jpg"

                Case "Granules"

                    Return "Images/granules.jpg"

                Case "Paste"

                    Return "Images/Paste2.jpg"

                Case Else

                    Return "Images/questionmark.jpg"

            End Select

     

        End Function

     

        Public Function ConvertBack(ByVal value As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack

            Throw New NotImplementedException()

        End Function

    End Class 

    As before, the parameter named 'value' is an Object type which is the key element in the conversion process. It contains the value that is to be converted. The conversion is simple in this case because all we need is to find the string value of the enumeration that is being passed in. So the basic ToString method will work fine.

    Once we have a String value, this is compared against the various possibilities. A string which represents the file path to an appropriate image is returned by the converter. I've chosen to include the image files in the project, but of course they could be stored externally and still be accessed in the same way.

    The next steps are the same as for the previous examples in the earlier blogs. First, map the current assembly to an XML namespace in Application.xaml:  

      xmlns:local="clr-namespace:WPFListView2"  

    Then create an instance of the converter class in Application.xaml and give it a key: 

        <local:MaterialToImagePathConverter x:Key="IconConverter" /> 

    Next, create a DataTemplate for the new column of the ListView:  

        <DataTemplate x:Key="IconCellTemplate">

                <Image Margin="0,0,1,3" Height="18" Width="25"

              Stretch="Fill"     

            Source="{Binding Path=Material, Converter={StaticResource IconConverter}}" />

        </DataTemplate> 

    In the above markup, the Binding Path is the Material field, the converter is the MaterialToImagePathConverter instance created above.

    Finally, the ListView in the Window needs to have a new column added in which the images can be displayed: 

              <GridViewColumn

               CellTemplate="{StaticResource IconCellTemplate}">

              </GridViewColumn> 

    And now you are all set. The finished Window displays as shown below:

     

       

     

    For completeness, the full markup for the Window which contains the ListView is shown here: 

    <Window x:Class="Window2"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       Title="Image Path Converter Demo" Height="360" Width="450">

      <Grid>

     

        <ListView Name="ProductsListView"

             ItemsSource="{Binding}"

               Margin="5" >

     

          <ListView.ItemContainerStyle>

            <Style TargetType="ListViewItem">

              <Setter Property="HorizontalContentAlignment" Value="Stretch" />

            </Style>

          </ListView.ItemContainerStyle>

     

          <ListView.View>

            <GridView>

              <!-- Icon column -->

              <GridViewColumn

               CellTemplate="{StaticResource IconCellTemplate}">

              </GridViewColumn>

              <!-- Product ID -->

              <GridViewColumn

               HeaderTemplate="{StaticResource IDColHeader}"

              CellTemplate="{StaticResource IDCellTemplate}">

              </GridViewColumn>

              <!-- Product Name -->

              <GridViewColumn

               HeaderTemplate="{StaticResource NameColHeader}"

               CellTemplate="{StaticResource NameCellTemplate}">

              </GridViewColumn>

              <!-- Pack Size -->

              <GridViewColumn

               HeaderTemplate="{StaticResource PackageColHeader}"

               CellTemplate="{StaticResource PackCellTemplate}">

              </GridViewColumn>

            </GridView>

          </ListView.View>

        </ListView>

     

      </Grid>

    </Window> 

    Of course you are not forced to use the ValueConverter approach for this task. You could rewrite the class and give it an ImagePath property. Then in the code-behind run a function with a similar Select Case block to the one used above, passing back the appropriate path to the ImagePath property. The collection would be amended on the fly to include this data and finally the amended collection would be used as the DataContext. The Binding Path for the Image Source property would be that field. Personally, I think the ValueConverter is neater. And if the data comes from an external source, perhaps in the form of an XML file, then using the ValueConverter is almost certainly a better way.

  • How To Insert Row Divider Lines in a WPF ListView

     

    If you have read any of my previous ListView blogs, you will know that formatting the ListView is mostly a matter of creating templates and styles for the various sub-elements. The same applies if you want to do something to differentiate one row from another. For instance, you might to have horizontal divider lines between each row.

    This is the look we are going for in this blog:

            

    The column headers were described in this blog, so I won't repeat the markup for those here.

    In my last ListView blog, the ItemContainerStyle was used and we looked at the HorizontalContentAlignment property of the ListViewItem. By setting it to Stretch, the width of the items in each column was stretched so that their start and finish points all lined up vertically.

    Here is the markup: 

          <ListView.ItemContainerStyle>

            <Style TargetType="ListViewItem">

                <Setter Property="HorizontalContentAlignment" Value="Stretch" />

            </Style>

          </ListView.ItemContainerStyle> 

    And the result was this:

            

     

    The problem with this obviously is those little gaps between the columns. The ListView inserts a margin of 6 units to the left and right of each column by default. So that is the cause of the gaps you can see there.

    Each cell used a DataTemplate. Here is the DataTemplate for the cells in the first column shown above: 

        <DataTemplate x:Key="IDCellTemplate2">

          <Border Background="LightGreen" >

          <TextBlock Foreground="MediumBlue"

                    FontFamily="Calibri"

             Text="{Binding Path=ProductID}" />

          </Border>

        </DataTemplate> 

    What we need to do is to continue to make use the Border element in the DataTemplate for the cell, but remove the Light Green Background color. Then we can set a BorderBrush of Black and give it a Thickness of 1 unit.

    Here's a first pass at it. This is the DataTemplate for the cells in the first column: 

        <DataTemplate x:Key="IDBorderedCellTemplate">

          <Border BorderBrush="Black"

                 BorderThickness="1" >

            <TextBlock Foreground="MediumBlue"

                    FontFamily="Calibri"

               Text="{Binding Path=ProductID}" />

          </Border>

        </DataTemplate> 

    The other two templates are very similar so I won't clog up the screen with them just yet. Here's the result so far:

     

           

    We ended up with boxes when we really wanted lines, but it's a good start. The first trick is to set the individual values on the BorderThickness - that is left, top, right and bottom. By setting a value of 0 on the left and right we will remove the vertical lines.

    We also want to remove the top border, otherwise there will be a double line between the rows (the bottom border of one row, followed by the top border of the next). So, here's an improved version: 

        <DataTemplate x:Key="IDBorderedCellTemplate">

          <Border BorderBrush="Black"

                  BorderThickness="0,0,0,1" >

            <TextBlock Foreground="MediumBlue"

                    FontFamily="Calibri"

               Text="{Binding Path=ProductID}" />

          </Border>

        </DataTemplate> 

           

    Getting closer. If we offset the start positions of the Borders in the DataTemplates and shunt them to the left to span the default gap of 6 units either side, we should be able to create a continuous line.

    The following code snippet is the DataTemplate for the second column's cells. I'll explain why in a moment:

          <DataTemplate x:Key="NameBorderedCellTemplate">

          <Border BorderBrush="Black" Margin="-12,5,0,1"

                 BorderThickness="0,0,0,1"  >

             <TextBlock Foreground="MediumBlue"

               FontFamily="Calibri" FontWeight="Bold"

               Text="{Binding Path=ProductName}" />

          </Border>

        </DataTemplate> 

    The result:

           

    What has happened here is that the Border in the second cell's template has been moved 12 units to the left. This has the effect of joining the end of first cell's bottom border to the start of the second cell's bottom border. This is achieved by setting the value of -12 for the left margin in the second line of markup above. So, to come back to why I have shown you the second cell's template, the reason is of course that the first cell doesn't need to be shunted to the left.

    It's almost perfect, but there is just one minor adjustment still needed which I'll deal with next.

    Moving the Border to the left means that its child TextBlock is also moved. It would look much better if the text content was displayed more in line with the column headers. You can see this in the second two columns in the screenshot above. This can be fixed by setting Margins on the TextBlocks. This time, I will shunt the TextBlocks back to the right.

    Here is the markup for the second cell again: 

          <DataTemplate x:Key="NameBorderedCellTemplate">

          <Border BorderBrush="Black" Margin="-12,5,0,1"

                 BorderThickness="0,0,0,1"  >

             <TextBlock Foreground="MediumBlue"

               FontFamily="Calibri" FontWeight="Bold"

               Margin="14,0,0,2"

               Text="{Binding Path=ProductName}" />

          </Border>

        </DataTemplate> 

    This time it is the Margin property on Line 6 that's of interest. As you can see, it pushes the start of the text back to where we want it.

           

    For completeness, here is the markup for all three DataTemplates: 

        <DataTemplate x:Key="IDBorderedCellTemplate">

          <Border BorderBrush="Black" Margin="0,5,0,1"

                BorderThickness="0,0,0,1" >

            <TextBlock Foreground="MediumBlue"

                FontFamily="Calibri"

                Margin="3,0,0,2"

                Text="{Binding Path=ProductID}" />

          </Border>

        </DataTemplate>

     

          <DataTemplate x:Key="NameBorderedCellTemplate">

           <Border BorderBrush="Black" Margin="-12,5,0,1"

               BorderThickness="0,0,0,1"  >

             <TextBlock Foreground="MediumBlue"

               FontFamily="Calibri" FontWeight="Bold"

               Margin="14,0,0,2"

               Text="{Binding Path=ProductName}" />

           </Border>

        </DataTemplate>

     

        <DataTemplate x:Key="PackBorderedCellTemplate">

          <Border BorderBrush="Black" Margin="-12,5,0,1"

                 BorderThickness="0,0,0,1" >

             <TextBlock Foreground="MediumBlue"

                FontFamily="Calibri"

                Margin="15,0,0,2"

                Text="{Binding Path=PackageType}" />

          </Border>

        </DataTemplate> 

    As it has been a couple of blogs since you saw the markup for the actual ListView, I'll list that too: 

        <ListView Name="ProductsListView"

             ItemsSource="{Binding}"

               Margin="5,25" >

          <ListView.ItemContainerStyle>

            <Style TargetType="ListViewItem">

                <Setter Property="HorizontalContentAlignment" Value="Stretch" />

            </Style>

          </ListView.ItemContainerStyle>

     

          <ListView.View>

            <GridView>

              <!-- Product ID -->

              <GridViewColumn

               HeaderTemplate="{StaticResource IDColHeader}"

              CellTemplate="{StaticResource IDBorderedCellTemplate}">

              </GridViewColumn>

              <!-- Product Name -->

              <GridViewColumn

               HeaderTemplate="{StaticResource NameColHeader}"

               CellTemplate="{StaticResource NameBorderedCellTemplate}">

              </GridViewColumn>

              <!-- Pack Size -->

              <GridViewColumn

               HeaderTemplate="{StaticResource PackageColHeader}"

               CellTemplate="{StaticResource PackBorderedCellTemplate}">

              </GridViewColumn>

            </GridView>

          </ListView.View>

        </ListView> 

    The DataBinding to the very basic data source was covered in this blog.

    So I know I've plodded through this in minute detail and you might think that it would have been better to have shown you the final versions of those DataTemplates and cut out all the intermediate steps. The reason I haven't is that you might want to take the basic idea and tweak it further - change colors, alter the vertical space between lines, change the distance that the text is shunted and so on. Now that you fully understand what each of those individual tweaks does, you can go ahead and make those kind of changes without having to pull my XAML apart to work out how its done.

  • More Complex Conversions with the WPF MultiBinding Converter

    In the previous blog, I created a simple ValueConverter that analyzed the data bound values and set the Foreground color of a TextBlock based on whether the value was less or more than a cut off value of 200 units. In reality, these preset values (such as the 200 in that demonstration) are rarely that rigid and you often need some flexibility in the analysis. To continue the example of a collection of drink products, it might be more realistic to compute a cut off value on the fly. For instance, if you know your annual sales for an individual item and you know the quantity you have in stock, you might want to highlight those items that need to be re-ordered. If it's a big selling item, the value of 200 may be far too low as a cut off; conversely an item with low annual sales might only need to be re-ordered when the stock is down to a handful.

    So, how do we include this kind of calculation in a process where the value is data bound, but we need to apply some arithmetic to more than one field? The answer is to use MultiBinding and a MultiValueConverter.

    You create a class that implements IMultiValueConverter. The core difference between this class and the one created in the previous blog is that the 'value' parameter of the Convert method is renamed to 'values' and is an array, not a single object. Armed with that array, you can pass in the values of two or more of the fields of your data source, run the calculations and Return a result.

    Take a look at this class:

    Public Class StockLevelConverter

        Implements IMultiValueConverter

     

        Public Function Convert(ByVal values() As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert

     

            If targetType IsNot GetType(Brush) Then Return Nothing

     

            '  Variables for the arithmetic operations

            Dim QtyHeld As Integer = CInt(values(0))

            Dim AnnualSales As Integer = CInt(values(1))

     

            '  Avoid divide by zero errors by not proceeding to the calculation

            '  and returning a different brush

            If QtyHeld = 0 Or AnnualSales = 0 Then Return Brushes.DarkGray

     

            Dim RestockLevel As Integer = AnnualSales / 12

     

            Return (If(QtyHeld < RestockLevel, Brushes.Red, Brushes.MediumBlue))

     

        End Function

     

        Public Function ConvertBack(ByVal value As Object, ByVal targetTypes() As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack

            Throw New NotImplementedException()

        End Function

    End Class

    It is similar to the converter class created in the previous blog, having the same two methods, Convert and ConvertBack.    Note that this class implements IMultiValueConverter and the first parameter being an array as mentioned earlier.

    In the Convert method, I have included the test that confirms that the correct Target Type has been passed in. Two variables, QtyHeld and AnnualSales are created and you will see that their respective values are taken from the two elements of the array named 'values' passed in as a parameter. We will need to look at how those array elements are notified to the converter in a moment.

    Continuing with the Convert method, a test is included to avoid any divide by zero errors. I have chosen to Return a different colored Brush, but it could have been left to the default of MediumBlue if preferred.

    The 'RestockLevel' variable could have been left out and the calculation that it uses could have been placed inside the If statement in the final line. But I felt that it is easier to read in the more verbose approach. The gist of these two lines is to divide the annual sales figure by 12 in order to find the average monthly sales. If the Stock held is less than one month's worth, then a Red Brush is returned, otherwise a MediumBlue one.

    So, how do the values in the array get passed in to the Convert method? The answer is that a MultiBinding is used in the XAML. This is a similar approach to the single ValueConverter example seen previously, except that this time two Paths are required - one for the Quantity field and one for the AnnualSales field of the data source.

    Assuming you are using the same project as in the previous blog, there is already a namespace mapping named 'local'. You then need to create an instance of this new converter class:

        <local:StockLevelConverter x:Key="StockChecker" />

    The DataTemplate for the Quantity column cells will contain the MultiBinding. Here is the markup for this: 

        <DataTemplate x:Key="QtyCellTemplate">

          <TextBlock

              Text="{Binding Path=Quantity}" >

            <TextBlock.Foreground>

              <MultiBinding Converter="{StaticResource StockChecker}">

                <Binding Path="Quantity" />

                <Binding Path="AnnualSales" />

              </MultiBinding>

            </TextBlock.Foreground>

          </TextBlock>

        </DataTemplate> 

    The Element.Property syntax is used to allow for the multiple sub elements. The MultiBinding is created and set to point to that instance of the StockLevelConverter class, which has been keyed as 'StockChecker'. The two paths which are passed in to the StockLevelConverter are listed next. Note that the array is filled in the order in which these paths are listed - that is values(0) will take the Quantity field values and values(1) will take the AnnualSales field values.

    That's all there is to it. If I create some arbitrary dummy data for all the properties of the DrinkProduct class collection (i.e. including the Quantity and AnnualSales figures) and run the project, the resulting ListView will look like this:

     

    If you're interested in trying this out, using the class code from the previous blog but can't be bothered to create your own dummy data, here is the collection I created:

     

      Public Shared Function StockCheck() As List(Of DrinkProduct)

            Dim CurrentProducts As New List(Of DrinkProduct)

            With CurrentProducts

                .Add(New DrinkProduct("CF1kg", "Coffee Powder", "1 Kg", MaterialType.Powder, 15684, 1276))

                .Add(New DrinkProduct("CFB500", "Ground Coffee", "500 g", MaterialType.Powder, 22785, 12856))

                .Add(New DrinkProduct("CFG500", "Coffee Granules", "500 g", MaterialType.Granules, 19233, 5907))

                .Add(New DrinkProduct("Te500", "Tea", "500 g", MaterialType.Leaf, 8544, 235))

                .Add(New DrinkProduct("TeInst500", "Instant Tea", "500 g", MaterialType.Powder, 1009, 22))

                .Add(New DrinkProduct("SMlk1lt", "Skimmed Milk", "1 Litre", MaterialType.Liquid, 28012, 2650))

                .Add(New DrinkProduct("HiJ300", "HiJuice Drink Mix", "300 g", MaterialType.Other, 578, 179))

                .Add(New DrinkProduct("Sm400", "Smoothie", "400ml", MaterialType.Paste, 9346, 3284))

                .Add(New DrinkProduct("Beef300", "Beef Drink", "300 g", MaterialType.Granules, 8316, 1965))

                .Add(New DrinkProduct("Beef750", "Beef Drink", "750 g", MaterialType.Granules, 7612, 359))

     

            End With

     

            Return CurrentProducts

        End Function

     

    Don't forget to include the DataContext in the code-behind of Window1, the Window that holds the actual ListView:

    Class Window1

        Dim CurrentProducts As New List(Of DrinkProduct)

     

     

        Private Sub Me_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

            CurrentProducts = DrinkProduct.StockCheck()

            Me.DataContext = CurrentProducts

        End Sub

    End Class

More Posts Next page »
Copyright 1998-2009 vbCity.com LLC
Powered by Community Server (Non-Commercial Edition), by Telligent Systems