Search code examples
vb.netwinformslistbox

Listbox breaks when setting it to a virtual instance from a class


I have a weird problem that I can't wrap my head around.

I have the following code:

Public Class Form1
    Public WithEvents MyClass1 As New MyClass

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

    Private Sub MyClass_UpdateListbox() Handles MyClass1.UpdateListbox
        For Each sItem as String In MyClass1.Listbox
            MsgBox(sItem)                        'an MsgBox shows correct items each time.
        Next sItem
        Me.Listbox = Me.MyClass1.Listbox         'doesn't work and breaks listbox.
        Me.Listbox.Items.Clear()                 'listbox is empty anyway, but has no effect.
        Me.Listbox.Items.Add("event triggered")  'does nothing.
    End Sub
End Class

Public Class MyClass
    Public Listbox as new Listbox
    Public Event UpdateListbox()

    Public Sub New()
        'Constructor. sub.
        Me.AddItem("Populating listbox")
    End Sub

    Public Sub AddItem(sItem as String)
        Me.Listbox.Items.Add(sItem)
        RaiseEvent UpdateListbox()
    End Sub
End Class

If I comment the following lines in above code, the listbox keeps adding event triggered, as expected. Of course, I don't have to remove the clear one. It will work, but then it just adds the same item. If I use a command button and call MyClass.AddItem("Something") that is correctly added too as long as the below is commented out. But if not, then once the listbox is in broken state, nothing can be added anymore.

Me.Listbox = Me.MyClass1.Listbox      'doesn't work and breaks listbox.
Me.Listbox.Items.Clear()              'listbox is empty anyway, but has no effect.

How can I use a virtual listbox and assign it to my real listbox?

Also, instead of assigning one listbox to the other, I can of course use that for each loop and add each item one by one which works, but that for each look was for debugging purpose in the first place.

EDIT:
My goal with this application is to build a Todo list with features that are not in a todolist. This is a project I build for work because there I need a tool like this. I already have a todolist that I use but I built it wrong in the past. Everything was condensed in form1, no modules no extra classes. As a result I got weird bugs that I patched with workarounds. I am now rebuilding the application from the ground up, separating tasks in its own classes so I can apply business logic and have a true OOP application. The todo list will become its own class, and managing the list etc will be handeled by this class. It interacts with controls on the form, such as buttons and listboxes. If I just use form1.listbox from the class, things break at program start. I started another question and the below code was a now deleted answer. At first I did not get it working because I did not realize the listbox crashes if I assign it the virtual instance.

So my goal is to have the todolist be handled entirely by the todolist class. It does need a way to interact with controls on form1, and that is the puzzle I'm currently trying to solve.


Solution

  • In the original code, the main problem is that the Field that hold the instance of a Control shown if a Form is reassigned to the instance of another ListBox Control defined in a custom class:

    Me.Listbox = Me.MyClass1.Listbox 
    

    From now on, Me.Listbox points another ListBox that is not show on screen, so any attempt to update the Form's child ListBox fails, except when Me.Listbox.Items.Clear() is called - in the same procedure - after it's being reassigned, because the handle of the Owner of the ObjectCollection (the object that holds the Items shown in the ListBox) has not been updated yet. It's going to fail after the current method exits nonetheless.


    As noted in comments, this is a simplified method to handle a Form and its child Controls using a handler class. The contract between the class handler and a Form is sealed by an Interface (named IFormHandler here).
    A Form that implements this Interface exposes the methods defined by the Interface that allow to trigger Actions and specific behaviors, depending on the Type of Control and the implementation.

    I suggest to take a look at the MVP or ReactiveUI (MVVM-derived) for WinForms Patterns.


    How too proceed:

    Open up the ApplicationEvents class object.
    If you don't have it already, select Project -> Properties -> Application and click the View Application Events button. It will generate ApplicationEvents.vb. Find it in Solution Explorer and open it up.

    It should look like this (plus a bunch of comments that explain what it's for):

    Imports Microsoft.VisualBasic.ApplicationServices
    
    Namespace My
        Partial Friend Class MyApplication
    
        End Class
    End Namespace
    

    Paste into MyApplication these lines of code:

    Imports Microsoft.VisualBasic.ApplicationServices
    
    Namespace My
        Partial Friend Class MyApplication
            Public SomeFormHandler As MyFormHandler(Of SomeForm)
    
            Protected Overrides Function OnStartup(e As StartupEventArgs) As Boolean
                SomeFormHandler = New MyFormHandler(Of SomeForm)
                Return MyBase.OnStartup(e)
            End Function
        End Class
    End Namespace
    

    Add an Interface that defines the Actions (or Behaviors) that a Form must implement.
    Here, the GetUsersList() method specifies that a Form that implements this Interface must return the instance of a child ListBox Control.
    (To add an Interface, select Project -> Add -> New Item... and select the Interface template. Name the file IFormHandler)

    Extend this Interface as needed, to add more Methods or Properties that define actions and behaviors.

    Public Interface IFormHandler
        Function GetUsersList() As ListBox
    End Interface
    

    A Form that implements the IFormHandler Interface implements and exposes the GetUsersList() method, which returns the instance of a ListBox Control (named usersList here)

    There's nothing else to do with this Form, the control is handed over to the MyFormHandler object that is initialized with this Type.

    Public Class SomeForm
        Implements IFormHandler
    
        Public Sub New()
            InitializeComponent()
        End Sub
    
        Public Function GetUsersList() As ListBox Implements IFormHandler.GetUsersList
            Return Me.usersList
        End Function
    End Class
    

    Now, to show SomeForm, you can use the MyFormHandler class object show below.

    ' Set the Owner if called from another Form
    My.Application.SomeFormHandler.Show(Me) 
    ' Or without an Owner
    My.Application.SomeFormHandler.Show()
    

    To close SomeForm, you can either use its handler:

    My.Application.SomeFormHandler.Close()
    

    or close it as usual:

    [SomeForm Instance].Close()
    

    If MyFormHandler determines that the instance of SomeForm has been disposed, it creates a new one when you call its Show() method again later.

    To update the ListBox Control of SomeForm, use the public methods exposed by the MyFormHandler class:

    ' Add a new element
    My.Application.SomeFormHandler.UpdateUsersList(UpdateType.AddElement, "Some Item")
    
    ' Remove an element
    My.Application.SomeFormHandler.UpdateUsersList(UpdateType.RemoveElement, "Some Item")
    
    ' Replace an element
    My.Application.SomeFormHandler.UpdateUsersList(UpdateType.ReplaceElement, "New Item", "Some Item")
    
    ' Clears the ListBox  
    My.Application.SomeFormHandler.ClearUsersList()
    

    All these actions generate an event that you can subscribe to when needed.
    See also the example that shows how to raise a custom event when the ListBox raises one of its stardard events; SelectedIndexChanged is handled here.
    See the implementation of MyFormHandler.

    Generic Form handler:
    A Form needs to implement the IFormHandler Interface for the MyFormHandler class to accept it as valid.
    You can of course extend the Interface, to add more Actions, or build a MyFormHandler class object that uses a different Interface, or more than one.

    Public Class MyFormHandler(Of TForm As {Form, IFormHandler, New})
        Implements IDisposable
    
        Private formObject As TForm
        Private IsInstanceSelfClosing As Boolean = False
        Public Event UsersListUpdate(item As Object, changeType As UpdateType)
        Public Event UsersListIndexChanged(index As Integer)
    
        Public Sub New()
            InitializeInstance()
            Dim lstBox = formObject.GetUsersList()
            AddHandler lstBox.SelectedIndexChanged, AddressOf OnUsersListIndexChanged
        End Sub
    
        Private Sub InitializeInstance()
            formObject = New TForm()
            AddHandler formObject.FormClosing, AddressOf OnFormClosing
        End Sub
    
        Private Sub OnFormClosing(sender As Object, e As FormClosingEventArgs)
            IsInstanceSelfClosing = True
            Dispose()
        End Sub
    
        Public Sub UpdateUsersList(updateMode As UpdateType, newItem As Object, Optional oldItem As Object = Nothing)
            If newItem Is Nothing Then Throw New ArgumentException("New Item is null")
    
            Dim lstBox = formObject.GetUsersList()
            Select Case updateMode
                Case UpdateType.AddElement
                    lstBox.Items.Add(newItem)
                Case UpdateType.RemoveElement
                    lstBox.Items.Remove(newItem)
                Case UpdateType.ReplaceElement
                    If oldItem Is Nothing Then Throw New ArgumentException("Replacement Item is null")
                    Dim index = lstBox.Items.IndexOf(oldItem)
                    lstBox.Items.Remove(oldItem)
                    lstBox.Items.Insert(index, newItem)
                Case Else : Return
            End Select
            RaiseEvent UsersListUpdate(newItem, updateMode)
        End Sub
    
        Public Sub ClearUsersList()
            formObject.GetUsersList().Items.Clear()
        End Sub
    
        Private Sub OnUsersListIndexChanged(sender As Object, e As EventArgs)
            RaiseEvent UsersListIndexChanged(DirectCast(sender, ListBox).SelectedIndex)
        End Sub
    
        Public Sub Show(Optional owner As IWin32Window = Nothing)
            If formObject Is Nothing OrElse formObject.IsDisposed Then InitializeInstance()
            If formObject.Visible Then
                formObject.WindowState = FormWindowState.Normal
                formObject.BringToFront()
            Else
                formObject.Show(owner)
            End If
        End Sub
    
        Public Sub Close()
            If formObject IsNot Nothing AndAlso (Not formObject.IsDisposed) Then
                RemoveHandler formObject.FormClosing, AddressOf OnFormClosing
                IsInstanceSelfClosing = False
                Dispose()
            End If
        End Sub
    
        Public Sub Dispose() Implements IDisposable.Dispose
            Dispose(True)
            GC.SuppressFinalize(Me)
        End Sub
    
        Protected Overridable Sub Dispose(disposing As Boolean)
            If disposing Then
                If formObject Is Nothing OrElse formObject.IsDisposed Then Return
                Dim lstBox = formObject.GetUsersList()
                RemoveHandler lstBox.SelectedIndexChanged, AddressOf OnUsersListIndexChanged
                RemoveHandler formObject.FormClosing, AddressOf OnFormClosing
                If Not IsInstanceSelfClosing Then formObject.Close()
                IsInstanceSelfClosing = False
            End If
        End Sub
    End Class
    

    Enumerator used in MyFormHandler:

    Public Enum UpdateType
        AddElement
        RemoveElement
        ReplaceElement
    End Enum