vb.netbuttondynamicselectionlistviewitem

Select List View Items Using Dynamically Created Buttons - Code Limitations?


enter image description here

After days of searching on the web and checking all SO similar questions here, I have a working version of the code I'm looking for! :-) But, it's very limited as I describe below! The reason is, I can't find a way to code for the way I want my program to work! The code below has taken me days and many attempts to get a working version! I'm using WIN10, Visual Studio 2019, VB.NET. Grateful to TEME64 for the example of creating buttons dynamically dynamic buttons. Does anyone know a more concise code to achieve the questions I have below? I would greatly appreciate your help!

What I need:

dynamic buttons w/ icons

The little program I'm trying to create 1. Dynamically creates buttons from the total number of List View items (i.e. this is already working). 2. It should then extract and apply program icons for each List View subitem to the dynamic buttons on a Flow Layout Panel (i.e. got this working for one button at a time, but I don't know how to assign a different program icon to each button from each of the paths in List View subitems)? 3. Then, when you click each button, it should start a different external program (i.e. from paths in List View subitems - i.e. Process Start below). My List View columns are: NAME = Program Name; PATH = Program Exec file.

Question 1: How do I write more concise code? I tried changing the item's Index number dynamically using a Numeric Up Down control, but I couldn't get this working? I.e. only the first item remains selected and only the first program opens??? Question 2: How do I apply the program icons from each listview item to the dynamic button's background image independently of how many items there are in the List View or dynamic buttons which will be the same amount? The List View is sorted alphabetically!
Question 3: How do I handle user Cancel for external programs?

What I have tried:

Creating Buttons Dynamically:

The following code creates a button for each List View item. The buttons are contained on a Flow Layout Panel separately from the List View. The minus 1 is because the code was creating 4 buttons for 3 List View Items:

Dim i As Integer

    For i = 0 To ListView1.Items.Count.ToString - 1 '30 = number of buttons to create!!!
        NewButton(i)
    Next i

Use Buttons to Select Different List View Items and Run Programs From Paths:

The following code actually works! But, it is very limited in several ways: 1. Repetitive: It's old school long hand. So, I need to repeat the code for each button/List View item! I don't want to limit the number of List View items or buttons created from them? A more objective code would be nice! 2. ButtonNumber: Each button identifies itself by number (i.e. 0, 1, 2) but I don't know how to connect this to each List View item's index number (SelectedIndices?) in order to select each List View item using its corresponding button (i.e. button with program icon derived from path in List View)? 3. Exception: The second program runs (i.e. Iobit Smart Defrag), but if I click Cancel on the Smart Defrag's startup dialog my code throws an exception which I don't know how to handle because it's an external program?

If ButtonNumber.ToString = 0 Then
        
        ListView1.SelectedItems.Clear()
        ListView1.SelectedIndices.Clear()
        ListView1.FocusedItem = ListView1.Items(0)
        ListView1.FocusedItem.Selected = True
        ListView1.FocusedItem.EnsureVisible()
        ListView1.Focus()

        Process.Start(ListView1.SelectedItems.Item(0).SubItems(1).Text.ToString())

    ElseIf ButtonNumber.ToString = 1 Then
        
        ListView1.SelectedItems.Clear()
        ListView1.SelectedIndices.Clear()
        ListView1.FocusedItem = ListView1.Items(1)
        ListView1.FocusedItem.Selected = True
        ListView1.FocusedItem.EnsureVisible()
        ListView1.Focus()

        'USER CANCEL THROWS EXCEPTION - HOW DO YOU HANDLE THIS???
        Process.Start(ListView1.SelectedItems.Item(0).SubItems(1).Text.ToString())

    ElseIf ButtonNumber.ToString = 2 Then
        
        ListView1.SelectedItems.Clear()
        ListView1.SelectedIndices.Clear()
        ListView1.FocusedItem = ListView1.Items(2)
        ListView1.FocusedItem.Selected = True
        ListView1.FocusedItem.EnsureVisible()
        ListView1.Focus()

        Process.Start(ListView1.SelectedItems.Item(0).SubItems(1).Text.ToString())

    End If

Suggested Code Adaptations:

I saved my original code to file and added the suggested code. I have made a few alterations to the code in an attempt to achieve my objective for the program (See new diagram above). However, although the alterations got me closer to what I need I currently can only get the results I want by selecting a LVW item and pressing the Get Selected Item button for each button to be added and their corresponding programs to run? Any ideas what I still need to do with the code to get it to: 1. Create a button for each LVW item; 2. Run a program from the selected item's subitem (i.e. exec Path)? 3. Pressing each button should select the corresponding LVW item and run the program?

The Adapted Code:

Changes: 1. Adapted myApps to load from LVW1 and added For Each loop; 2. Removed Button Text from create buttons because I only want the icon; 3. Added button size; 4. Moved LoadMyAppsList() and CreateButtons(myApps) from Form_Load to a button temporarily.

    Private Sub LoadMyAppsList()

'List Source = ListView1:
        For Each item As ListViewItem In ListView1.Items

            myApps = New List(Of ApplicationDetails)
            myApps.Add(New ApplicationDetails With
                       {.ApplicationName = ListView1.SelectedItems.Item(0).Text.ToString,
                       .ApplicationShortcut = ListView1.SelectedItems.Item(0).SubItems(1).Text.ToString(),
                       .ApplicationIcon = Icon.ExtractAssociatedIcon(ListView1.SelectedItems.Item(0).SubItems(1).Text.ToString())})
        Next

    End Sub

    Private Sub CreateButtons(myApps As List(Of ApplicationDetails))

        For Each appdet As ApplicationDetails In myApps

            Dim shortcutButton As Button = New Button With
                {.Image = appdet.ApplicationIcon.ToBitmap,
                .Tag = appdet.ApplicationShortcut}
            AddHandler shortcutButton.Click, AddressOf CommonButton_Click
            FlowLayoutPanel1.Controls.Add(shortcutButton)

            'Added - Button Size:
            shortcutButton.Width = 63
            shortcutButton.Height = 63

            MsgBox(appdet.ApplicationShortcut.ToString)

        Next

        'Removed:
        '.Text = appdet.ApplicationName,

    End Sub

    Private Sub Button5_Click(sender As Object, e As EventArgs) Handles Button5.Click

        ListView1.BeginUpdate()

        If ListView1.Items.Count > 0 AndAlso ListView1.SelectedItems.Count > 0 Then

            LoadMyAppsList()
            CreateButtons(myApps)

        End If

        ListView1.EndUpdate()

    End Sub

LVW Item Selection Using Buttons?

After working with the code for quite a while I discovered a way to get the corresponding LVW items to select when their buttons are clicked. I'm sure there's probably a better way, but this works! What I need now is a way to open all the buttons in one go creating them from the LVW items. Currently, I need to manually select a LVW item, click the Get Selected Item button and a button is created. Then, repeat this for each button creation??? I've tried changing the LVW code lines from SelectedItems to Items, but this doesn't fix the problem??? The following code works for selecting LVW items:

Private Sub CommonButton_Click(sender As Object, e As EventArgs)

    'Use variable to get exe path from button tag:
    Dim exePath As String = DirectCast(sender, Button).Tag.ToString

    'Find lvw item which contains same path as button tag:
    Dim item1 As ListViewItem = ListView1.FindItemWithText(exePath.ToString)
    
    'Select lvw item which corresponds to button: 
    ListView1.SelectedIndices.Clear()
    ListView1.FocusedItem = ListView1.Items(item1.Text.ToString)
    ListView1.FocusedItem.Selected = True
    ListView1.FocusedItem.EnsureVisible()
    ListView1.Focus()

    'Run program:
    Process.Start(exePath)

End Sub

THE APP

My Favourite Apps List

THE CODE:

I spent a lot of time debugging through the first code I found on Daniweb and also Andrew's code suggestions below to understand both of them. But, the code I have submitted below is a "no bells and whistles" version (i.e. minimal code for the app to work as proposed) that I managed to get working which is an adaptation of the code from Daniweb dynamic buttons:

'APP SETUP:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

    'LVW SETUP:
    Me.ListView1.View = View.Details
    Me.ListView1.HideSelection = False
    ListView1.FullRowSelect = True
    ListView1.GridLines = True
    ListView1.Sorting = SortOrder.Ascending

    'SELECT FIRST LVW ITEM: 
    'COMMENT: Commented out lines which don't
    'affect first LVW item selection!!!
    'ListView1.SelectedIndices.Clear()
    ListView1.FocusedItem = ListView1.Items(0)
    ListView1.FocusedItem.Selected = True
    'ListView1.FocusedItem.EnsureVisible()
    ListView1.Select()
    'ListView1.Focus()

    'AUTORESIZE Name And Path COLUMNS
    Me.ListView1.AutoResizeColumn(0, ColumnHeaderAutoResizeStyle.ColumnContent)
    Me.ListView1.AutoResizeColumn(1, ColumnHeaderAutoResizeStyle.ColumnContent)

End Sub

'LOAD BUTTONS BUTTON:
Private Sub Button8_Click(sender As Object, e As EventArgs) Handles Button8.Click

    'CLEAR BUTTONS TO PREVENT DUPLICATES:
    FlowLayoutPanel1.Controls.Clear()

    'GET LVW ITEM'S TEXT:
    Dim username As String
    Dim session As String
    Dim output As String

    For Each item As ListViewItem In Me.ListView1.Items

        username = item.Text
        session = item.SubItems.Item(1).Text
        output = session 'username + " : " + session

        'OUTPUT RESULTS TO CONSOLE:
        Console.WriteLine(output)

        'EXTRACT PROGRAM EXE FILES FROM LVW SUBITEM PATHS:
        Dim ico As Icon = Icon.ExtractAssociatedIcon(output)

        'CREATE AND ADD NEW BUTTONS:
        Dim btn As New Button
        FlowLayoutPanel1.Controls.Add(btn)
        btn.BackgroundImage = ico.ToBitmap
        btn.BackgroundImageLayout = ImageLayout.Stretch
        btn.Size = New Size(63, 63)

        'ADD PATHS FROM LVW SUBITEMS TO EACH BUTTON TAG:
        btn.Tag = output

        'HANDLE DYNAMIC BUTTON'S CLICK (i.e. Links to dynamic buttons click event)
        AddHandler btn.Click, AddressOf onButtonClick

    Next

End Sub

'DYNAMIC BUTTONS CLICK - RUN PROGRAMS FROM BUTTONS:
Private Sub onButtonClick(ByVal sender As System.Object, ByVal e As System.EventArgs)

    'UPDATE LVW:
    ListView1.BeginUpdate()

    'CHECK SENDING BUTTON'S TAG:
    If CType(sender, Button).Tag IsNot Nothing Then

        'FIND LVW SUBITEM PATH WHICH CORRESPONDS TO PATH IN BUTTON TAG:
        Dim item1 As ListViewItem = ListView1.FindItemWithText(CType(sender, Button).Tag.ToString)

        'CHECK PATHS CORRESPOND:
        If (item1 IsNot Nothing) Then

            With ListView1

                'BUTTON CLICK SELECTS CORRESPONDING LVW ITEM: 
                .SelectedIndices.Clear()
                .FocusedItem = item1
                .FocusedItem.Selected = True
                .FocusedItem.EnsureVisible()
                .Focus()

            End With

            'RUN CORRESPONDING PROGRAM - PTL!!!
            Process.Start(ListView1.SelectedItems.Item(0).SubItems(1).Text.ToString)

            'THIS ALSO RUNS EACH PROGRAM CORRESPONDING WITH BUTTONS-PTL!!!
            'Process.Start(CType(sender, Button).Tag.ToString)

        Else

            'USER NOTIFICATION:
            MessageBox.Show("Item not found!")

        End If

    End If

    'END UPDATE LVW:
    ListView1.EndUpdate()

End Sub

Andrew Mortimer's Code:

I have tried the new code, but after renaming according to the naming convention I have used, adding the path to the CSV file, etc., I get the following exception: System.InvalidCastException: 'Conversion from string "ASC;C:\Program Files (x86)\IObit" to type 'Integer' is not valid.' on the following code block:

myApps.Add(New ApplicationDetails With
                   {.ID = values(0),
                   .ApplicationName = values(1),
                   .ApplicationShortcut = values(2),
                   .ApplicationIcon = Icon.ExtractAssociatedIcon(values(2))
                   })

Solution

  • Hopefully this will get you close to what you need.
    It is using a custom class to store your application details.
    It uses a common button click event handler to handle all the click events.

    Imports System.IO
    Public Class ButtonsForm
    
        Private myApps As List(Of ApplicationDetails)
        Private maxAppId As Integer
    
        Private Class ApplicationDetails
    
            Public Property ID As Integer
            Public Property ApplicationName As String
            Public Property ApplicationShortcut As String
            Public Property ApplicationIcon As Icon
    
            ''' <summary>
            ''' Comma delim property to use when writing out app file
            ''' </summary>
            ''' <returns></returns>
            Public ReadOnly Property WriteLine As String
                Get
                    Return String.Join(",", ID, ApplicationName, ApplicationShortcut)
                End Get
            End Property
    
        End Class
    
        Private Sub CommonButton_Click(sender As Object, e As EventArgs)
    
            'get the application id
            Dim searchID As Integer = DirectCast(sender, Button).Tag
    
            'find it in the listview
            For Each lv As ListViewItem In AppListView.Items
                If lv.Tag = searchID Then
                    lv.Selected = True
                    LaunchApplication(lv.SubItems(1).Text)
                End If
            Next
    
        End Sub
    
        Private Shared Sub LaunchApplication(applicationPath As String)
    
            Process.Start(applicationPath)
    
        End Sub
    
        Private Sub LoadMyAppsListFromCSV()
    
            Dim csvFilePath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "MyApps.csv")
    
            'initialise app list
            myApps = New List(Of ApplicationDetails)
    
            Dim lines() As String = File.ReadAllLines(csvFilePath)
    
            For Each line In lines
                Dim values() As String = line.Split(Convert.ToChar(","))
                myApps.Add(New ApplicationDetails With
                           {.ID = values(0),
                           .ApplicationName = values(1),
                           .ApplicationShortcut = values(2),
                           .ApplicationIcon = Icon.ExtractAssociatedIcon(values(2))
                           })
            Next
    
            'get the max app id to be used if another application is added
            maxAppId = myApps.Max(Function(x) x.ID)
    
        End Sub
    
        Private Sub CreateButtons(myApps As List(Of ApplicationDetails))
    
            'clear the controls
            ButtonsFLP.Controls.Clear()
    
            'add back
            For Each appdet As ApplicationDetails In myApps
    
                Dim shortcutButton As Button = New Button With
                    {.Text = appdet.ApplicationName,
                    .Image = appdet.ApplicationIcon.ToBitmap,
                    .Tag = appdet.ID}
                AddHandler shortcutButton.Click, AddressOf CommonButton_Click
                ButtonsFLP.Controls.Add(shortcutButton)
    
            Next
    
        End Sub
    
        Private Sub InitFormButton_Click(sender As Object, e As EventArgs) Handles InitFormButton.Click
    
            InitForm()
    
        End Sub
    
        Private Sub InitForm()
    
            LoadMyAppsListFromCSV()
            CreateButtons(myApps)
            CreateListView(myApps)
    
        End Sub
    
        Private Sub CreateListView(myApps As List(Of ApplicationDetails))
    
            AppListView.Items.Clear()
    
            For Each appdet As ApplicationDetails In myApps
    
                Dim lvitem As ListViewItem = AppListView.Items.Add(appdet.ApplicationName)
                lvitem.SubItems.Add(appdet.ApplicationShortcut)
                lvitem.Tag = appdet.ID
    
            Next
    
        End Sub
    
        Private Sub AddAppButton_Click(sender As Object, e As EventArgs) Handles AddAppButton.Click
    
            Try
                Dim myNewApp As ApplicationDetails = New ApplicationDetails() With
                        {.ApplicationName = ApplicationNameTextBox.Text,
                        .ApplicationShortcut = ApplicationPathTextBox.Text
                }
                If ValidForAdd(myNewApp) Then
                    AddApplication(myNewApp)
                    InitForm()
                End If
    
            Catch ex As Exception
                MessageBox.Show(String.Concat("An error occurred: ", ex.Message))
            End Try
    
        End Sub
    
        Private Sub AddApplication(myNewApp As ApplicationDetails)
    
            'get the next id
            myNewApp.ID = maxAppId + 1
            'add to the collection
            myApps.Add(myNewApp)
            'rewrite the collection
            WriteAppCollectionToCsv(myApps)
    
        End Sub
    
        Private Sub WriteAppCollectionToCsv(myApps As List(Of ApplicationDetails))
    
            Dim csvFilePath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "MyApps.csv")
            Dim backupFile As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, String.Format("MyApps_{0}.csv", Guid.NewGuid.ToString()))
            'backup the current file just in case..
            File.Move(csvFilePath, backupFile)
            'write the new file
            Using sw As New StreamWriter(csvFilePath)
                For Each item As ApplicationDetails In myApps
                    sw.WriteLine(item.WriteLine)
                Next
            End Using
    
        End Sub
    
        Private Function ValidForAdd(myNewApp As ApplicationDetails) As Boolean
    
            If myNewApp.ApplicationName.Trim.Length = 0 Then
                MessageBox.Show("Please enter an application name", Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Warning)
                ApplicationNameTextBox.Focus()
                Return False
            End If
    
            If myNewApp.ApplicationShortcut.Trim.Length = 0 Then
                MessageBox.Show("Please enter an application path", Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Warning)
                ApplicationPathTextBox.Focus()
                Return False
            End If
    
            Dim appExists As Boolean = myApps.Any(Function(x) x.ApplicationName = myNewApp.ApplicationName And x.ApplicationShortcut = myNewApp.ApplicationShortcut)
            If appExists Then
                MessageBox.Show("You've added this already", Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Warning)
                ApplicationNameTextBox.Focus()
                ApplicationNameTextBox.SelectAll()
                Return False
            End If
    
            Return True
    
        End Function
    
        Private Sub RemoveAppButton_Click(sender As Object, e As EventArgs) Handles RemoveAppButton.Click
    
            Try
                If AppListView.SelectedItems.Count = 0 Then
                    MessageBox.Show("Please select an application to remove", Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Warning)
                    Return
                End If
    
                If DeleteSelectedApp() Then
                    InitForm()
                End If
    
            Catch ex As Exception
                MessageBox.Show(String.Concat("An error occurred: ", ex.Message))
            End Try
    
        End Sub
    
        Private Function DeleteSelectedApp() As Boolean
    
            'get the id
            Dim delId As Integer = DirectCast(AppListView.SelectedItems(0).Tag, Integer)
    
            Dim appItemToRemove As ApplicationDetails = myApps.Where(Function(x) x.ID = delId).First
            myApps.Remove(appItemToRemove)
            WriteAppCollectionToCsv(myApps)
    
            Return True
    
        End Function
    
    End Class