in

vbCity Blogs

New (temp) place for vbCity Blogs

Ged Mead's Blog

October 2009 - Posts

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

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