Home > Enterprise >  Listbox breaks when setting it to a virtual instance from a class
Listbox breaks when setting it to a virtual instance from a class

Time:06-08

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.

CodePudding user response:

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