Implement Undo/Redo operations for Adding/Deleting ListView Items (Part 2)
Asked Answered
W

1

1

Well here I am to talk about the second part of this question Implement Undo/Redo operations for Adding/Deleting ListView Items and this other Extend this Class to Undo/Redo in a Listview.

I'm trying to implement Undo/Redo operations for Adding/Deleting ListView Items.

I've advanced a little more with the coding of this LV UndoManager code but it is very hard for me always when I try to advance.

At the moment I can add single items and then I can Undo/Redo perfectly that ADDED items, no more.

The problems that I have are these:

· When I remove a single Item from the Listview I'm unable to perform a "undo" to add again that item removed into the LV.

· When I add a range if items I can't undo, When I call UndoLastAction it throws a System.Reflection.TargetParameterCountException exception

· When I remove a range of Items I'm unable to undo/redo the operation and launchs the same exception.

In resume, If I add a single Item I can undo/redo perfecly, if I remove a single Item I can't undo right, also I can't undo/redo a range of ListViewItems.

I need someone who could help me to fix those problems... or at least one of them, with patience.

The code is a little big so I think that maybe can take less time to understand and to find errors opening and testing this source project that I've uploaded.

Here is the full Source:

http://elektrostudios.tk/UndoManager%20Test%20Application.zip

Just an image:

enter image description here

here is the UndoManager class:

Class ListView_UndoManager

    Private action As ListView_Action = Nothing

    Public Property Undostack As New Stack(Of ListView_Action)
    Public Property Redostack As New Stack(Of ListView_Action)

    ' Public Property IsDoingUndo As Boolean = False
    ' Public Property IsDoingRedo As Boolean = False

    ''' <summary>
    ''' Undo the last action.
    ''' </summary>
    ''' <remarks></remarks>
    Sub UndoLastAction()

        If Undostack.Count = 0 Then Exit Sub ' Nothing to Undo.

        action = Undostack.Pop ' Get the Action from Stack and remove it.
        action.Operation.DynamicInvoke(action.data) ' Invoke the undo Action.

    End Sub

    ''' <summary>
    ''' Redo the last action.
    ''' </summary>
    ''' <remarks></remarks>
    Sub RedoLastAction()

        If Redostack.Count = 0 Then Exit Sub ' Nothing to Redo.

        action = Redostack.Pop() ' Get the Action from Stack and remove it.
        action.Operation.DynamicInvoke(action.data) ' Invoke the redo Action.

    End Sub

End Class

Class ListView_Action

    ''' <summary>
    ''' Name the Undo / Redo Action
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Property name As String

    ''' <summary>
    ''' Points to a method to excecute
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Property Operation As [Delegate]

    ''' <summary>
    ''' Data Array for the method to excecute
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Property data As ListViewItem()

End Class

Here is the ListView user control that I'm using, I post this because is important the Events which I'm triggering: ItemAdded, ItemRemoved, RangeItemAdded and RangeItemRemoved.

Public Class LV : Inherits ListView

Public Shared Event ItemAdded As EventHandler(Of ItemAddedEventArgs)
Public Class ItemAddedEventArgs : Inherits EventArgs
    Public Property Item As ListViewItem
End Class

Public Shared Event ItemRemoved As EventHandler(Of ItemRemovedEventArgs)
Public Class ItemRemovedEventArgs : Inherits EventArgs
    Public Property Item As ListViewItem
End Class

Public Shared Event RangeItemAdded As EventHandler(Of RangeItemAddedEventArgs)
Public Class RangeItemAddedEventArgs : Inherits EventArgs
    Public Property Items As ListViewItem()
End Class

Public Shared Event RangeItemRemoved As EventHandler(Of RangeItemRemovedEventArgs)
Public Class RangeItemRemovedEventArgs : Inherits EventArgs
    Public Property Items As ListViewItem()
End Class

Public Sub New()

    Me.Name = "ListView_Elektro"
    Me.GridLines = True
    Me.FullRowSelect = True
    Me.MultiSelect = True
    Me.View = View.Details

End Sub

''' <summary>
''' Adds an Item to the ListView,
''' to monitor when an Item is added to the ListView.
''' </summary>
Public Function AddItem(ByVal Item As ListViewItem) As ListViewItem

    RaiseEvent ItemAdded(Me, New ItemAddedEventArgs With { _
                             .Item = Item
                       })

    Return MyBase.Items.Add(Item)

End Function

''' <summary>
''' Adds a range of Items to the ListView,
''' to monitor when an Item is added to the ListView.
''' </summary>
Public Sub AddItem_Range(ByVal Items As ListViewItem())

    RaiseEvent RangeItemAdded(Me, New RangeItemAddedEventArgs With { _
                                  .Items = Items
                            })

    MyBase.Items.AddRange(Items)

End Sub

''' <summary>
''' Removes an Item from the ListView
''' to monitor when an Item is removed from the ListView.
''' </summary>
Public Sub RemoveItem(ByVal Item As ListViewItem)

    RaiseEvent ItemRemoved(Me, New ItemRemovedEventArgs With { _
                               .Item = Item
                         })

    MyBase.Items.Remove(Item)

End Sub

''' <summary>
''' Removes a range of Items from the ListView
''' to monitor when an Item is removed from the ListView.
''' </summary>
Public Sub RemoveItem_Range(ByVal Items As ListViewItem())

    RaiseEvent RangeItemRemoved(Me, New RangeItemRemovedEventArgs With { _
                                    .Items = Items
                              })

    For Each Item As ListViewItem In Items
        MyBase.Items.Remove(Item)
    Next

End Sub

End Class

And finally here is the Form1 code of the Test application, here are the stuff which I use to add/remove items and to call undo/redo but I call to methods of my custom ListView user control so you need to notice that...:

Public Class Form1

    Dim _undoManager As New ListView_UndoManager
    Dim LVItem As ListViewItem

    Delegate Sub AddDelegate(item As ListViewItem)
    Delegate Sub RemoveDelegate(item As ListViewItem)

    Delegate Sub AddRangeDelegate(item As ListViewItem())
    Delegate Sub RemoveRangeDelegate(item As ListViewItem())

    ' Adds a single item
    Private Sub Button_AddItem_Click(sender As Object, e As EventArgs) _
    Handles Button_AddItem.Click

        Dim index As String = CStr(LV1.Items.Count + 1)

        LVItem = New ListViewItem With {.Text = index}
        LVItem.SubItems.AddRange({"Hello " & index, "World " & index})

        LV1.AddItem(LVItem)

    End Sub

    ' Adds a range of 2 items to the ListView
    Private Sub Button_AddRange_Of_Items_Click(sender As Object, e As EventArgs) Handles Button_AddRange_Of_Items.Click

        Dim index As String = CStr(LV1.Items.Count + 1)

        Dim lvitem As New ListViewItem With {.Text = index}
        lvitem.SubItems.AddRange({"Hello " & index, "World " & index})

        Dim lvitem2 As New ListViewItem With {.Text = index + 1}
        lvitem2.SubItems.AddRange({"Hello " & index + 1, "World " & index + 1})

        LV1.AddItem_Range({lvitem, lvitem2})

    End Sub

    ' Removes the last item
    Private Sub Button_RemoveLastItem_Click(sender As Object, e As EventArgs) _
    Handles Button_RemoveLastItem.Click

        If LV1.Items.Count <> 0 Then

            LV1.RemoveItem(LV1.Items.Cast(Of ListViewItem).Last)

        End If

    End Sub

    ' Clear all items
    Private Sub Button_Clear_Items_Click(sender As Object, e As EventArgs) _
    Handles Button_Clear_Items.Click

        LV1.Items.Clear()

    End Sub

    ' Clear the Undo/Redo Stacks
    Private Sub Button_Clear_Stacks_Click(sender As Object, e As EventArgs) _
    Handles Button_Clear_Stacks.Click

        _undoManager.Undostack = New Stack(Of ListView_Action)
        _undoManager.Redostack = New Stack(Of ListView_Action)

        Label_UndoCount_Value.Text = CStr(0)
        Label_RedoCount_Value.Text = CStr(0)

    End Sub

    ' Refreshes the Stacks Count
    Private Sub Refresh_StackCount()

        Label_UndoCount_Value.Text = CStr(_undoManager.Undostack.Count)
        Label_RedoCount_Value.Text = CStr(_undoManager.Redostack.Count)

    End Sub

    ' Monitors when an Item is added
    Private Sub ListView_ItemAdded(sender As Object, e As LV.ItemAddedEventArgs) _
    Handles LV1.ItemAdded

        ' // Crate an Undo Action
        Dim u As New ListView_Action()
        With u
            .name = "Remove Item"
            .Operation = New RemoveDelegate(AddressOf LV1.RemoveItem)
            .data = {e.Item}
        End With

        _undoManager.Undostack.Push(u)

        Refresh_StackCount()

    End Sub

    ' Monitors when a range of Items are added
    Private Sub ListView_RangeItemAdded(sender As Object, e As LV.RangeItemAddedEventArgs) _
    Handles LV1.RangeItemAdded

        ' // Crate an Undo Action
        Dim u As New ListView_Action()
        With u
            .name = "Remove Item Range"
            .Operation = New RemoveRangeDelegate(AddressOf LV1.RemoveItem_Range)
            .data = e.Items
        End With

        _undoManager.Undostack.Push(u)

        Refresh_StackCount()

    End Sub

    ' Monitors when an Item is removed
    Private Sub ListView_ItemRemoved(sender As Object, e As LV.ItemRemovedEventArgs) _
    Handles LV1.ItemRemoved

        ' // Create a Redo Action
        Dim r As New ListView_Action()
        With r
            .name = "Add Item"
            .Operation = New AddDelegate(AddressOf LV1.AddItem)
            .data = {e.Item}
        End With

        _undoManager.Redostack.Push(r)

        Refresh_StackCount()

    End Sub

    ' Monitors when a range of Items are removed
    Private Sub ListView_RangeItemRemoved(sender As Object, e As LV.RangeItemRemovedEventArgs) _
    Handles LV1.RangeItemRemoved

        ' // Create a Redo Action
        Dim r As New ListView_Action()
        With r
            .name = "Add Item"
            .Operation = New AddRangeDelegate(AddressOf LV1.AddItem_Range)
            .data = e.Items
        End With

        _undoManager.Redostack.Push(r)

        Refresh_StackCount()

    End Sub

    ' Undo
    Private Sub Button_Undo_Click(sender As Object, e As EventArgs) _
    Handles Button_Undo.Click

        _undoManager.UndoLastAction()

    End Sub

    ' Redo
    Private Sub Button_Redo_Click(sender As Object, e As EventArgs) _
    Handles Button_Redo.Click

        _undoManager.RedoLastAction()

    End Sub

    Private Sub Button_Remove_Range_Of_Items_Click(sender As Object, e As EventArgs) Handles Button_Remove_Range_Of_Items.Click

    If LV1.Items.Count > 1 Then

        Dim lvi1 As ListViewItem = LV1.Items(LV1.Items.Count - 1)
        Dim lvi2 As ListViewItem = LV1.Items(LV1.Items.Count - 2)

        LV1.RemoveItem_Range({lvi1, lvi2})

    End If


    End Sub

End Class

PS: Like I've said, really would be very more friendly to downlaod the source and test it.

Wessex answered 4/11, 2013 at 19:59 Comment(4)
I like your GUI Design!Katharynkathe
@Katharynkathe thanks but is not an own design, it is just because the dark theme that I use in Windows.Wessex
why isnt AddRange, just a For/Next loop on AddItem? Why the need for 2 more interfaces?Sacttler
@Plutonix good logic, thankyou... I will improve it. maybe that can be a solution to fix some things. Well I'm testing it now, when I add a range I can't undo the entire range of items, it just goes one by one... but at least it works and I'm happy with that, one thing less.Wessex
B
2

When I remove a single Item from the Listview - easy one.

RemoveItem removes an item from the list AND adds it to the ReDo stack, but it still also resides on the UnDo stack!!! If you Add 5, remove 1 and then Undo, you get 2 copies of item 5 on the Redo!

First, you should change the AddItem mechanism to a straight counter to make debugging easier

    nLVItemIndex += 1
    Dim index As String = (nLVItemIndex).ToString

    newItem = New ListViewItem
    newItem.Text = "Item " & index
    newItem.SubItems.Add("Hello " & index)
    newItem.SubItems.Add("World " & index)

    AddItem(newItem)

Using CStr on ListView item count creates names that can already exist on the UnDo/Redo stack and makes debugging more difficult.

I should think a GUI level, user invoked action like RemoveItem would fall into the UnDo stack. You are equating AddItem with UnDO and RemoveItem with Redo which is wrong. Everything from the GUI Form level should fall into the Undo stack, and the only way it should get into the ReDo is via a UM.Undo method.

Moving it to the UnDo stack will reveal another problem: Your UnDo Manager does very little for itself and uses the form level AddItem/RemoveItem rather than its own internal procedures (he cant even create his own UnDo/Redo Actions.) The result is that ALL Additem actions Push a Remove Action onto the UnDo stack; and All RemoveItems push a ReDo action which is NOT valid since you do want to UnDo a Remove!

The end result is that UM.UndoLastAction pops from UnDo (good) then DynamicInvoke triggers Form.AddItem which issues an UnDo Push (very bad because one was just popped - in fact thats what we are still doing - thats why the original had IsRedoing flags). UnDo Manager needs major brain surgery to do his own work because GUi level Add/Remove actions are not the same as UnDo/ReDo.

  • GUI Add Item ----> Push a remove action
  • GUI Remove ----> Push an add action
  • UM Pop Add ------> Add item; Push Remove onto ReDo
  • UM Pop Remove ------> Remove; Push Add onto Redo

This then reveals that UnDoManager doesnt have reference to the control he is "managing" let alone the ability to monitor more than one LV. I would think that an AddRange approach would just aggravate the issues above (cant find the essentials in the wall of code).

Finally, is it really necessary to post all the prop XML comment headers in the wall of text? Are all the Draw overrides germane to the Undo? No.

EDIT

Here is roughly what UnDoManager.UnDo needs to do (from my rework of that overblown one you started with):

Friend Function UnDo() As Boolean
    If _undoStack.Count = 0 Then Exit Function

    Dim ur As UndoAction         ' ie Command

    _IgnoreChange = True          ' ie IsUnDoing so you dont Push while Popping
    ur = _undoStack.Pop           ' get the Undo, such as LV.RemoveItem
    ur.Undo()                     ' Undo whatever it is (could be a checkbox etc)
    _IgnoreChange = False         ' open for business
    _redoStack.Push(ur)           ' push the same Action onto the ReDo
                                  ' I dont bother changing a code (yet) because
                                  ' if it is in Undostack it is an UnDo
   return True
End Function

My UnDoAction is just the Control being undone and the Data As Object. Since MOST Controls have only 1 thing a user messes with, no problem. LV has 2 legitimate user actions (Checked and Label Edit) so to be able to do either, it would need to be expanded.

Mine and the other one rely on polymorphism where undoStack(2) might be a checkedlistbox undo action and undoStack(9) might be a combox action - the watchers(monitors) KNOW which type to create AND how to Undo/ReDo the action. A Text Undo (TextBox, Combo, MaskedEdit and DateTimePicker) is just:

Friend Overrides Function Undo() As Boolean

    _Ctl.Text = _UndoData
    Return True

End Function

What I wonder about is now you are just doing LastItem - what about RemoveSelectedItem? How do you put it back in order? If you keep ANY sort of order ref it might be invalid because that ref might not be there anymore either.

Baca answered 4/11, 2013 at 21:31 Comment(7)
That "easy one" is easy to understand but not easy to solve it,I can't solve it,I'm trying now other point of view I've created 2 events in the undomanager class,the events are "undone" and "redone",undone is triggered when I Pop undostack and the redone the same for the redostack,I will attempt to create a redo actions only when Undone event is triggered and I will make a undo action on ItemAdded and ItemRemoved LV events but I have a mess, always an error everywhere nothing can work as expected... well, do you think I'm wasting time with this logic or is not a bad idea at all? ATM dont workWessex
fiiiiiiiiuuuu... finally I think I get the undomanager working right with the new logic, here in my country is too late so tomorrow I will update with the new code, I would like to know your observattions/suggestions when I've updated the new code if you could, maybe you could see an error in the code to fix it, thankyou @Plutonix.Wessex
a) is there a (good) reason why LV undos go into a different stack than all other controls? will yours also do Label Edit and Checkbox? b) Added 'UnDoManager.UnDo' info c) best to make a new questionSacttler
A) I don't get you, you will mean that I better merge undostack and redostack?, and yes ofcourse I will implement label editing and check/uncheck checkboxes... I go step by step... so slow. C) Really i'm not gonna make any more questions for this... no boy could write the full code and when I pass days writting something I need to rewrite it entirely again. no way.Wessex
Why doesnt your LV draw checkboxes? Or where did you hide it? I was dinking with how to use one UnDoAction to capture Text AND Check AND Item changes as an exercise. Hard to debug with no checks.Sacttler
Because like I've said I go step by step, the process of coding the undmanager is slow for my skills, I can't add all the necessary checks at the same time if the most important check (Add/remove items) does not work as expected at the moment. I've seen the edits of your answer, but I need time to try all, I have more projects to do also. thankyou for your time and your helpWessex
OK! I thought that was accounted for or that I broke something. I worked out how to remove selected item and put it back, an easy Check undo, and Label edit undo which is harder than it looks because LV hides WHICH item is being editted in the event and says the oldValue is NOTHING instead of whatever you set as Text. Check undo was problematic too because LV fires the ItemChecked event when you add a new item. ... so NOW I am ready for one of those bounty questions, LOL.Sacttler

© 2022 - 2024 — McMap. All rights reserved.