问题
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:
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.
回答1:
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.
来源:https://stackoverflow.com/questions/19776014/implement-undo-redo-operations-for-adding-deleting-listview-items-part-2