Extend this Class to Undo/Redo in a Listview
Asked Answered
L

3

8

I'm using a thir party code to manage undo/redo operations in a WindowsForm project.

I need to extend the Class to manage undo/redo operations in a Listview, this means:

· Undo/Redo Add/Delete items and subitems

· Undo/Redo Check/Uncheck rows

· Undo/Redo some other importants things that maybe I've missed

I don't know how to start doing this, the code is too complex for me, any help/tips/examples about this would be very gratified for me, but in 3 months I have not been able to carry out this change, I think I will need good explanations or full examples, here is the code:

********************************************************
 Undo/Redo framework (c) Copyright 2009 Etienne Nijboer
********************************************************

http://pastebin.com/Gmh5HS4x

(I did not post the code here because it exceeds the 30.000 character limit of StackOverflow)

UPDATE:

This is some usefull information from the Author explaining the things that I need to do to add the Listview support, but really I can't by myself:

Adding functionality for a listview shouldn't be that hard by the way and also a great way of getting into how it works. You would need to create a new monitor that will capture the listview change events and store the current value before it is changed. A command is created if you detect that a change was made with all the info needed to undo or redo the action. Thats it. As long as your monitor and command inherit from the base classes it will be detected and used automatically.

http://www.codeproject.com/Articles/43436/Undo-Redo-Framework

UPDATE:

The owner of the class updated the code adding one of the things that I needed, the label item undo/redo operations that I've asked for.

· Undo/Redo Text changes inside Listview (normal mode or details mode)

Unafortunatelly this update is not suficient for me to be able to add the other undo/redo operations that I need, please read @Plutonix comments explainning things

Here is the part of the updated Class for someone who can take ideas and help to extend it:

'****************************************************************************************************************
' ListView Undo/Redo Example, (c) Copyright 2013 Etienne Nijboer
'****************************************************************************************************************
' This is an example implementation of the Monitor and Command to add support for listviewitem labeltext changes
' Only the two classes arre needed to add support for an additional control. There were no extra changes needed
' in other code because the UndoRedoManager class uses reflection to discover the new Monitor and if you check 
' the message box on startup you'll notice the new addition of the ListViewMonitor to the list.
'
' Hopefully this example makes it easier for others to understand the mechanism behind this and how to add 
' undo/redo functionality for other actions and controls.
'
' Note: Beware that this example doesn't work if items in the listview can be sorted, moved and/or deleted. You
'       would need to expand the Monitor for these actions and add Command classes as well. Hopefully this 
'       addition to will make it easier for you to do just that ;-)
'
'   Good luck!
'
'****************************************************************************************************************

' Because we want to perform undo on a specific item at a certain index within the listview it is important this
' index is also stored. Otherwise we know that a label is changed but not to which item it belongs
Structure ListViewUndoRedoData
    Public ItemIndex As Integer
    Public LabelText As String
End Structure

'****************************************************************************************************************
' ListViewMonitor
'****************************************************************************************************************
Public Class ListViewMonitor : Inherits BaseUndoRedoMonitor

    Private Data As ListViewUndoRedoData

    Public Sub New(ByVal AUndoRedoManager As UndoRedoManager)
        MyBase.New(AUndoRedoManager)
    End Sub

    Public Overrides Function Monitor(ByVal AControl As System.Windows.Forms.Control) As Boolean
        If TypeOf AControl Is ListView Then
            AddHandler CType(AControl, ListView).BeforeLabelEdit, AddressOf ListView_BeforeLabelEdit
            AddHandler CType(AControl, ListView).AfterLabelEdit, AddressOf ListView_AfterLabelEdit
            Return True
        End If
        Return False
    End Function


    Private Sub ListView_BeforeLabelEdit(sender As System.Object, e As System.Windows.Forms.LabelEditEventArgs)
        ' Before change, make sure to save the data of what it is you want to be able to undo later.  
        Data.ItemIndex = e.Item
        Data.LabelText = CType(sender, ListView).Items(e.Item).Text
    End Sub


    Private Sub ListView_AfterLabelEdit(sender As System.Object, e As System.Windows.Forms.LabelEditEventArgs)
        ' Events that are also fired when the undo/redo value is changed by code, like change events,
        ' it is important to make sure that no undo/redo command is added when performing a undo/redo action.         
        If Not isPerformingUndoRedo Then            
            If Not (Data.ItemIndex = e.Item And String.Equals(Data.LabelText, e.Label)) Then
                AddCommand(UndoRedoCommandType.ctUndo, New ListViewUndoRedoCommand(Me, sender, Data))
                ListView_BeforeLabelEdit(sender, e)
            End If
        End If
    End Sub

End Class



'****************************************************************************************************************
' ListViewUndoRedoCommand
'****************************************************************************************************************
Public Class ListViewUndoRedoCommand : Inherits BaseUndoRedoCommand

    Public Sub New(ByVal AUndoMonitor As BaseUndoRedoMonitor, ByVal AMonitorControl As Control)
        MyBase.New(AUndoMonitor, AMonitorControl)
        Debug.Assert(False, "This constructor cannot be used because creating the current state of the control should be done at the actual undo or redo action!")
    End Sub

    Public Sub New(ByVal AUndoMonitor As BaseUndoRedoMonitor, ByVal AMonitorControl As Control, ByVal AUndoRedoData As Object)
        MyBase.New(AUndoMonitor, AMonitorControl, AUndoRedoData)
    End Sub

    Public ReadOnly Property Control() As ListView
        Get
            Return CType(UndoRedoControl, ListView)
        End Get
    End Property


    Private ReadOnly Property Data() As ListViewUndoRedoData
        Get
            Return CType(UndoRedoData, ListViewUndoRedoData)
        End Get
    End Property


    Private Function GetCurrentStateData() As ListViewUndoRedoData        
        GetCurrentStateData.ItemIndex = Data.ItemIndex
        GetCurrentStateData.LabelText = Control.Items(Data.ItemIndex).Text
    End Function


    Public Overrides Sub Undo()
        MyBase.Undo(GetCurrentStateData())
        Control.Items(Data.ItemIndex).Text = Data.LabelText
    End Sub

    Public Overrides Sub Redo()
        MyBase.Redo(GetCurrentStateData())
        Control.Items(Data.ItemIndex).Text = Data.LabelText
    End Sub

    Public Overrides Function CommandAsText() As String
        Return String.Format("Item {0}: {1}", Data.ItemIndex, Data.LabelText)
    End Function
End Class

UPDATE 2:

This is what the author said about how to add the functionalities that I need for the listview undo/redo operations:

I don't think you need to rewrite the full class. The hardest part of this, is finding a way of detecting when an item might be up for removal and when it is actually deleted. In the ListViewMonitor you will need to add the necessary event handlers (In the source where you find the AddHandler for BeforeLabelEdit and AfterLabelEdit). For the Command class you need to have the actual ListViewItem and the position of the item in the ListView before it was removed. You can simply create your structure with this information, something like ListViewItemRemoveUndoRedoData. When you undo the removal, you simply add the stored ListViewItem to the ListView at the position you stored with that. I would suggest adding an extra Count to the ListViewItemRemoveUndoRedoData structure that holds the number of items in the listview. Furthermore I think the only event you need is SelectedIndexChanged. When this event happens there are 2 situations.

1- The number of items is the same as the count stored earlier (set it to -1 or something on creation of the monitor): You store the item, position and the total item count.

2- The number of items is less than the count you stored earlier: An item is removed and you setup its UndoRedoCommand so it can be undone.

  • There is a 3th option of course, which would mean that an item is added

It needs some creativity to find the right events and what needs to be stored to perform the undo/redo. It might even mean that you need to find an alternative listview with better events and support (which you can find right here on codeproject)


UPDATE 3:

Trying to follow @ThorstenC solution, I'm getting problems with the RedoLastAction, it redos even if first I don't undo anything.

Also I can redo infinite times, and it only redos the last action, I mean If I undo 3 different LV items then I only can redo the last item added.

· UndoManager Class:

Class ListView_UndoManager

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

    Private action As ListView_Action = Nothing

    ''' <summary>
    ''' Undo the top of the stack
    ''' </summary>
    ''' <remarks></remarks>
    Sub UndoLastAction()

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

        action = Undostack.Pop ' Get the Action from Stack.
        action.Operation.DynamicInvoke(action.data) ' Invoke the reverse Action .

    End Sub

    ''' <summary>
    ''' Redo the top of the stack
    ''' </summary>
    ''' <remarks></remarks>
    Sub RedoLastAction()

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

        action = Redostack.Peek  ' Get the Action from Stack, but don't remove it.
        action.Operation.DynamicInvoke(action.data) ' Invoke the reverse 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 Object()

End Class

· Main Form Code:

' Undo/Redo
Dim _undoManager As New ListView_UndoManager
Delegate Sub RemoveDelegate(item As Object)
Delegate Sub AddDelegate(text As String, subtext1 As String, subtext2 As String)

' Button Add Song [Click]
Private Sub Button_Add_Song_Click(sender As Object, e As EventArgs) _
Handles Button_Add_Song.Click

    AddItem(ListView_Monitor.Items.Count + 1, WinampFile, ComboBox_Sendto.Text)

End Sub

Sub AddItem(ByVal name As String, ByVal subitem1 As String, ByVal subitem2 As String)

    Dim newItem = ListView_Monitor.Items.Add(name)
    newItem.SubItems.Add(subitem1)
    newItem.SubItems.Add(subitem2)

    'Crate an Undo Operation
    Dim u As New ListView_Action() With {.name = "Remove Item",
                        .Operation = New RemoveDelegate(AddressOf RemoveItem),
                                .data = New Object() {newItem}}

    _undoManager.Undostack.Push(u)

    ' Create a Redo        
    Dim r As New ListView_Action() With {.name = "Add Item",
                        .Operation = New AddDelegate(AddressOf AddItem),
                                .data = New Object() {name, subitem1, subitem2}}

    _undoManager.Redostack.Push(r)

End Sub

Sub RemoveItem(item As Object)
    ListView_Monitor.Items.Remove(item)
End Sub
Ligule answered 13/10, 2013 at 8:10 Comment(7)
I was considering this as a project, and I am not sure the underlying class is robust enough. For instance cbo changes are limited to changes to the text portion NOT the dropdown. It looks to be set up to track ONE thing per control which is fine for most ctls, but on a LV you potentially have 1)Checkstate, 2)New Item, 3) New Sub Item, 4) Group 5) Item or subitem text change. The BaseUndoRedoCommand class would need to be extended to handle all the things you want, and a new LV only UnDo proc. I keep thinking that the way an LV is that some undos would actually be a collection of commands!Brianna
Thanks again for comment @Plutonix, I did not understanded this part "a new LV only UnDo proc." what do you mean with "proc"?, also I did not noticed that the class does not has support for a dropdown CBO but you have reason and that means what extend the class would be harder than I ever imagined 'cause by my skills I'm not able to extend the class to add collection of commands, or just to add a simply one command for LV, damn.Ligule
the class is designed to undo user actions like to cbo.text, NOT changes via code (eg adding to cbo.items); tracking items/subitems on a LV is the same. The only thing it can easily UnDo is a Label (item) Edit or Check. Further, the LV is a collection of items - tracking changed text was easy, but required a new UndoCommand to know WHICH item to Undo (else use a new RM for each LV.Item). Given your requirement to use that UnDo class, it CANT do all the stuff you ticked off, but can at least track Item/Label edits. Still interested?Brianna
@Plutonix I appreciate your help but I think there is not necessary to edit the class to add the label undo/redo because I'have asked for help to the author and he added today an update to the class to do the label undo/redo operations, you can see the update in the codeproject url that I've posted in my question, then what I need is the rest operations (row deleteion and these) if that could be possibly to do, thanks for read.Ligule
Undo/Redo Add/Delete items and subitems this is not something the user can *directly DO - so UnDo probably exists in your code indirectly. E.g. a check to 'include Sub Dirs'; they toggle the check, your code responds to modify the contents. UnDo would conflict with your code and not a legitimate UnDo target. UnDo Check is valid, but since most controls have a single user edit content (Text, Selectedxxxxx, Checked ...) that class assumes a single content item, where LV has 2. I havent looked at his change, but it could probably be extended to handle checked as well...but thats all.Brianna
@Plutonix In the GUI I've added a context menu where this way I can "directly" delete a full row from a LV, also I've added some procedures to modify text on the cells and save it (serializing the listview contents in a file), when I delete a row or when I edit a cell text or when I checkbox a row I need to be able to undo/redo any of those 3 changes, you know I appreciate a lot your help but if you only could extend the class to undo/redo LV checkbox then I only could solve less-half of my problem. if you could extend it will be appreciated! but I could not mark the answer as accepted.Ligule
but this is too hard to find help to extend the class, The most important part for me is the undo/redo operations of adding/deleting rows/items/subitems, those are the most usually operations that I do in the LV, If someone could extend the class in that way I would be happy to accept the asnwer.Ligule
V
1

Try this approach: Forget this current implementation, start implementing your own Undo/Redo class.

Every Method that manipulates something needs to create it own Undo methods. Store the delegates and invoke it when required. I have make an example with a simple Add / Remove of listview items.

Public Class Form1

Dim _undoManager As New UndoManager

''' <summary>
''' Delegates to Remove an item
''' </summary>
''' <param name="rowNumber"></param>
''' <remarks></remarks>
Delegate Sub RemoveDelegate(item As Object)

''' <summary>
''' Delegates to Add an Item
''' </summary>
''' <param name="text"></param>
''' <remarks></remarks>
Delegate Sub AddDelegate(text As String)


Sub AddItem(name As String)


    Dim newItem = ListView1.Items.Add(name)

    'Crate an Undo Operation
    Dim a As New action() With {.name = "Remove Item",
                        .Operation = New RemoveDelegate(AddressOf RemoveItem),
                                .data = New Object() {newItem}}

    _undoManager.Undostack.Push(a)

    ' Create a Redo        
    Dim a As New action() With {.name = "Add Item",
                        .Operation = New AddDelegate(AddressOf AddItem),
                                .data = New Object() {name}}

    _undoManager.Redostack.Push(a)



End Sub

Sub RemoveItem(item As Object)
    ListView1.Items.Remove(item)
End Sub

''' <summary>
''' Changes the Text of the Item
''' </summary>
''' <param name="item"></param>
''' <param name="text"></param>
''' <remarks></remarks>
Sub changetext(item As Object, text As String)
    Dim oldtext As String = item.text

End Sub


Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    Me.AddItem("new Item")
End Sub

Private Sub Button2_Click(sender As System.Object, e As System.EventArgs) Handles Button2.Click
    _undoManager.UndoLastAction()
End Sub

End Class

Class UndoManager


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

''' <summary>
''' Undos the top of the stack
''' </summary>
''' <remarks></remarks>
Sub UndoLastAction()
    Dim action As action = Undostack.Pop ' Get the Action from Stack
    action.Operation.DynamicInvoke(action.data) ' Invoke the reverse Action 

End Sub

Sub RedoLastAction()
    Dim action As action = Redostack.Peek' Get the Action from Stack, but dont remove
    action.Operation.DynamicInvoke(action.data) ' Invoke the reverse Action 

End Sub


End Class

Class 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 Object()
End Class
Vigil answered 30/10, 2013 at 10:16 Comment(6)
The 'RemoveItem' and 'ChangeItem' methods should also add an action to the undoManager but for demo puroses I left them blank. Think you get the idea.Vigil
I'm having troubles with the "RedoLastAction", it always redo an action, even if first I don't do a Undo action, the redo action always is performed when I click in my "Redo" button. I've tried to use Redostack.Pop instead of Redostack.Peek to remove the action but does nothing, I can redo infinite times, like I say also I can redo even if first I do not undo any action.Ligule
Also it can only Redo the last operation, I mean if I undo 3 times (3 different LV items) and I try to redo 3 times then it only redos the last action so I when I redo 3 times I get the same LV item duplicated 3 times, redo code is not working as expected, please could you fix it? I need only the Redo fix to mark this answer as accepted, thankyou for your help and sorry for my English.Ligule
I've posted the piece of code in my update, please if you could see it... thanks.Ligule
fiuu... finally I could done it, really thankyou for your help.Ligule
please help here if you could, is good explained... #19757624Ligule
C
5

If look closely at line 328, it already handles a ListView. Is it lacking in some way?

Citronella answered 13/10, 2013 at 13:2 Comment(1)
Thanks for answer (maybe the answer better would be a comment), I've examined full the Class and I think just that line what you've seen is just to monitor the enter/leave events, I don't know exactly but sure is not for anything important related about undo/redo operations, you can not see any other line containing anything about Listview things in the Class, also the own Author said (in CodeProject site) that the Class does not has support for a Listview control, I need to add it by myself. The author could not help me ('cause he won't and he don't have time to do it) ,thanks for answerLigule
V
1

Try this approach: Forget this current implementation, start implementing your own Undo/Redo class.

Every Method that manipulates something needs to create it own Undo methods. Store the delegates and invoke it when required. I have make an example with a simple Add / Remove of listview items.

Public Class Form1

Dim _undoManager As New UndoManager

''' <summary>
''' Delegates to Remove an item
''' </summary>
''' <param name="rowNumber"></param>
''' <remarks></remarks>
Delegate Sub RemoveDelegate(item As Object)

''' <summary>
''' Delegates to Add an Item
''' </summary>
''' <param name="text"></param>
''' <remarks></remarks>
Delegate Sub AddDelegate(text As String)


Sub AddItem(name As String)


    Dim newItem = ListView1.Items.Add(name)

    'Crate an Undo Operation
    Dim a As New action() With {.name = "Remove Item",
                        .Operation = New RemoveDelegate(AddressOf RemoveItem),
                                .data = New Object() {newItem}}

    _undoManager.Undostack.Push(a)

    ' Create a Redo        
    Dim a As New action() With {.name = "Add Item",
                        .Operation = New AddDelegate(AddressOf AddItem),
                                .data = New Object() {name}}

    _undoManager.Redostack.Push(a)



End Sub

Sub RemoveItem(item As Object)
    ListView1.Items.Remove(item)
End Sub

''' <summary>
''' Changes the Text of the Item
''' </summary>
''' <param name="item"></param>
''' <param name="text"></param>
''' <remarks></remarks>
Sub changetext(item As Object, text As String)
    Dim oldtext As String = item.text

End Sub


Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    Me.AddItem("new Item")
End Sub

Private Sub Button2_Click(sender As System.Object, e As System.EventArgs) Handles Button2.Click
    _undoManager.UndoLastAction()
End Sub

End Class

Class UndoManager


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

''' <summary>
''' Undos the top of the stack
''' </summary>
''' <remarks></remarks>
Sub UndoLastAction()
    Dim action As action = Undostack.Pop ' Get the Action from Stack
    action.Operation.DynamicInvoke(action.data) ' Invoke the reverse Action 

End Sub

Sub RedoLastAction()
    Dim action As action = Redostack.Peek' Get the Action from Stack, but dont remove
    action.Operation.DynamicInvoke(action.data) ' Invoke the reverse Action 

End Sub


End Class

Class 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 Object()
End Class
Vigil answered 30/10, 2013 at 10:16 Comment(6)
The 'RemoveItem' and 'ChangeItem' methods should also add an action to the undoManager but for demo puroses I left them blank. Think you get the idea.Vigil
I'm having troubles with the "RedoLastAction", it always redo an action, even if first I don't do a Undo action, the redo action always is performed when I click in my "Redo" button. I've tried to use Redostack.Pop instead of Redostack.Peek to remove the action but does nothing, I can redo infinite times, like I say also I can redo even if first I do not undo any action.Ligule
Also it can only Redo the last operation, I mean if I undo 3 times (3 different LV items) and I try to redo 3 times then it only redos the last action so I when I redo 3 times I get the same LV item duplicated 3 times, redo code is not working as expected, please could you fix it? I need only the Redo fix to mark this answer as accepted, thankyou for your help and sorry for my English.Ligule
I've posted the piece of code in my update, please if you could see it... thanks.Ligule
fiuu... finally I could done it, really thankyou for your help.Ligule
please help here if you could, is good explained... #19757624Ligule
V
1

I see. It's a bit of how you define what a "Redo" should do. In your case you want to Redo an Undo operation. By default, a Redo repeats the last action. Even If you Undo something, the Redo Undos again. Try this approach: Understand the Code fragment only as building blocks. The "RemoveItem" Method don't add Code to Undo/Redo stack - Add this Undo Redo like in the Add - Method. If you don't need to 'Undo an Undo' - Operation, add a

Property IsDoingUndo as boolean

To UndoManager and set it true if doing an Undo. Check this Property in Add/Remove Method and don't add something to the Undo/Redo Stack. Like :

If not _UndoManager.IsDoingUndo then 
...
else
...
endif

With this you will get control of what should be undo-able and redo-able. Sorry that I can not provide sourcecode this time.

Vigil answered 3/11, 2013 at 10:31 Comment(1)
I can't follow. please if you could provide a fix.Ligule

© 2022 - 2024 — McMap. All rights reserved.