Implement undo / redo operations to add / remove ListView items (Part 2)

Well, I have to talk about the second part of this question. Introduce Undo / Redo operations to add / remove ListView elements , and another. Extend this class to undo / redo in Listview .

I am trying to perform Undo / Redo operations to add / remove ListView items.

I have made some progress with the encoding of this LV UndoManager code, but it is very difficult for me whenever I try to advance.

At the moment, I can add individual elements, and then I can completely undo / redo that ADDED elements are no more.

I have the following problems:

When I remove one item from the list, I can’t β€œcancel” to add this item to LV again.

When I add a range, if the elements cannot cancel, When I call UndoLastAction , it throws a System.Reflection.TargetParameterCountException exception

. When I delete a series of items, I cannot undo / redo the operation and throw the same exception.

In summary, if I add one item, I can quickly undo / redo it, if I delete one item that I cannot undo correctly, also I cannot undo / change the range of ListViewItems.

I need someone who can help me fix these problems ... or at least one of them with patience.

The code is a bit big, so I think it may take less time to understand and find errors when opening and testing this source project, which I downloaded.

Here is the full Source:

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

Simple 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 control that I use, I submit it, because the Events I fire are ItemAdded : 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, this is what I use to add / remove items and to call undo / redo, but I am referring to the methods of my custom ListView 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: As I said, it would really be very down-source friendly and test it.

+1
listview winforms undo-redo
source share
1 answer

When I remove a single Item from the Listview - Easy.

RemoveItem removes an item from the list And adds it to the ReDo stack, but it is still on the UnDo stack as well !!! If you add 5, delete 1, and then cancel, you will get 2 copies of paragraph 5 on repeat!

First, you must change the AddItem mechanism to a direct 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 in a ListView item creates names that already exist on the UnDo / Redo stack and makes debugging difficult.

I have to think about the GUI level, a user-invoked action, such as RemoveItem, will hit the UnDo stack. You equate AddItem with UnDO and RemoveItem with Redo, which is wrong. Everything, starting from the GUI form level, should get on the Undo stack, and the only method it should get in ReDo is the UM.Undo method.

Moving it to the UnDo stack will reveal another problem: your UnDo Manager is very small for itself and uses the AddItem / RemoveItem level of the form, rather than its own internal procedures (it cannot even create its own UnDo / Redo Actions). the result is ALL Additem actions. Press "Delete action" on the UnDo stack; and All RemoveItems click on the ReDo action, which is NOT valid since you want UnDo a Remove!

The end result is that UM.UndoLastAction comes out of UnDo (good), and then DynamicInvoke launches Form.AddItem, which issues UnDo Push (very bad, because one of them just popped up) - this is actually what we still doing - that’s why the original had IsRedoing flags). UnDo Manager needs serious brain surgery to do its own job, because the GUi level add / remove levels do not match UnDo / ReDo.

  • GUI Add Item ----> Click Uninstall Action
  • GUI Remove ----> Click the add action
  • UM Pop Add ------> Add item; Click Delete on ReDo
  • UM Pop Delete ------> Delete; Push add to redo

This shows that UnDoManager does not have a link to the control that it controls, not to mention the ability to track more than one LV. I would think that the AddRange approach would simply exacerbate the above problems (cannot find the basic information in the code wall).

Finally, is it really necessary to post all the XML comment comment headings in the text wall? Are all Draw overridden in Undo? Not.

EDIT

Here's roughly what UnDoManager.UnDo needs to do (from my refinement of this bloated 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 a Canceled Control and a Data As Object . Since MOST Controls have only one thing that the user encounters, no problem. LV has 2 legitimate user actions (Checked and Label Edit), in order to be able to do this, it would have to be expanded.

Mine, and the other relies on polymorphism, where undoStack (2) can be a canceled checklistbox action, and undoStack (9) can be a combox action - KNOW watchers (monitors), what type to create AND, like a Undo / ReDo action. Undoing text (TextBox, Combo, MaskedEdit and DateTimePicker) is simple:

 Friend Overrides Function Undo() As Boolean _Ctl.Text = _UndoData Return True End Function 

What interests me is, now you are just doing LastItem - how about RemoveSelectedItem? How did you get it back in order? If you save any type of order, it may not be valid, because this ref may not be there.

+2
source share

All Articles