【问题标题】:Implement Undo/Redo operations for Adding/Deleting ListView Items (Part 2)为添加/删除 ListView 项实现撤消/重做操作(第 2 部分)
【发布时间】:2013-11-15 13:27:59
【问题描述】:

这里我要谈谈这个问题的第二部分Implement Undo/Redo operations for Adding/Deleting ListView Items 和另一个Extend this Class to Undo/Redo in a Listview

我正在尝试为添加/删除 ListView 项实现撤消/重做操作。

我在这个 LV UndoManager 代码的编码方面已经进步了一点,但是当我尝试进步时,我总是很难。

目前我可以添加单个项目,然后我可以完美地撤消/重做添加的项目,仅此而已。

我遇到的问题是:

·当我从列表视图中删除单个项目时,我无法执行“撤消”以再次将已删除的项目添加到 LV 中。

·当我添加一个无法撤消的项目范围时,当我调用UndoLastAction 时,它会引发System.Reflection.TargetParameterCountException 异常

·当我删除一系列项目时,我无法撤消/重做操作并启动相同的异常。

在简历中,如果我添加一个项目,我可以完美地撤消/重做,如果我删除一个项目,我无法正确撤消,我也无法撤消/重做一系列 ListViewItem。

我需要有人能耐心地帮助我解决这些问题……或者至少其中一个问题。

代码有点大,所以我认为可能需要更少的时间来理解和发现打开和测试我上传的这个源项目的错误。

这里是full 来源:

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

只是一张图片:

这里是 UndoManager 类:

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

这是我正在使用的 ListView 用户控件,我发布这个是因为我触发的事件很重要:ItemAddedItemRemovedRangeItemAddedRangeItemRemoved

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

最后这里是测试应用程序的 Form1 代码,这里是我用来添加/删除项目和调用撤消/重做的东西,但我调用了我的自定义 ListView 用户控件的方法,所以你需要注意...:

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:就像我说的那样,下载源代码并测试它真的会更友好。

【问题讨论】:

  • 我喜欢你的 GUI 设计!
  • @user2896540 谢谢,但不是自己设计的,只是因为我在 Windows 中使用的深色主题。
  • 为什么不是 AddRange,只是 AddItem 上的 For/Next 循环?为什么还需要 2 个接口?
  • @Plutonix 逻辑很好,谢谢...我会改进它。也许这可以成为解决某些问题的解决方案。好吧,我现在正在测试它,当我添加一个范围时,我无法撤消整个项目范围,它只是一个接一个地进行......但至少它有效并且我对此感到满意,少了一件。

标签: .net vb.net winforms listview undo-redo


【解决方案1】:

When I remove a single Item from the Listview - 简单的一个。

RemoveItem 从列表中删除一个项目并将其添加到 ReDo 堆栈中,但它仍然驻留在 UnDo 堆栈中!!!如果您添加 5,删除 1,然后撤消,您将在重做中获得 2 个项目 5 的副本!

首先,您应该将 AddItem 机制更改为直接计数器,以使调试更容易

    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)

在 ListView 项目计数上使用 CStr 会创建可能已经存在于 UnDo/Redo 堆栈中的名称,并使调试更加困难。

我应该认为 GUI 级别、用户调用的操作(如 RemoveItem)会落入 UnDo 堆栈。您将 AddItem 等同于 UnDO 并将 RemoveItem 等同于 Redo,这是错误的。 GUI 表单级别的所有内容都应该落入 Undo 堆栈,并且它应该进入 ReDo 的唯一方式是通过 UM.Undo 方法。

将它移到 UnDo 堆栈将揭示另一个问题:您的 UnDo Manager 自己做的很少,并且使用表单级别的 AddItem/RemoveItem 而不是它自己的内部过程(他甚至不能创建自己的 UnDo/Redo Actions。)结果是所有 Additem 操作将 Remove Action 推入 UnDo 堆栈;并且所有 RemoveItems 都会推送一个无效的 ReDo 操作,因为您确实想要撤消删除!

最终结果是 UM.UndoLastAction 从 UnDo 弹出(好)然后 DynamicInvoke 触发 Form.AddItem 发出 UnDo Push (非常糟糕,因为刚刚弹出 - 事实上这就是我们仍在做的 - 这就是为什么原件有 IsRedoing 标志)。 UnDo Manager 需要进行脑部大手术来完成自己的工作,因为 GUI 级别的 Add/Remove 操作与 UnDo/ReDo 不同。

  • GUI 添加项目 ----> 推送删除操作
  • GUI 删除 ----> 推送添加操作
  • UM Pop 添加 ------> 添加项目;将 Remove 推送到 ReDo
  • UM 弹出删除 ------> 删除;将添加推送到重做

这表明 UnDoManager 没有引用他正在“管理”的控件,更不用说监控多个 LV 的能力了。我认为 AddRange 方法只会加剧上述问题(在代码墙中找不到要领)。

最后,真的有必要把所有的prop XML评论头都贴在文字墙里吗?所有的 Draw 覆盖都与 Undo 密切相关吗?没有。

编辑

这大概是UnDoManager.UnDo 需要做的事情(根据我对你开始时夸大其词的修改):

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

我的UnDoAction 只是正在撤消的控制和Data As Object。由于 MOST Controls 只有一件事让用户搞砸,所以没问题。 LV 有 2 个合法的用户操作(Checked 和 Label Edit),因此需要扩展它才能做到。

我的和另一个依赖于多态性,其中 undoStack(2) 可能是选中列表框撤消操作,而 undoStack(9) 可能是组合框操作 - 观察者(监视器)知道要创建哪种类型以及如何撤消/重做行动。文本撤消(TextBox、Combo、MaskedEdit 和 DateTimePicker)只是:

Friend Overrides Function Undo() As Boolean

    _Ctl.Text = _UndoData
    Return True

End Function

我想知道的是你现在只是在做 LastItem - RemoveSelectedItem 呢?你怎么把它放回去?如果您保留任何类型的订单 ref,它可能是无效的,因为该 ref 也可能不再存在。

【讨论】:

  • 那个“easy one”很容易理解但不容易解决,我解决不了,我现在正在尝试其他观点我在undomanager类中创建了2个事件,事件是“撤消”和“重做”,当我弹出撤消堆栈和重做相同的重做堆栈时触发撤消,我将尝试仅在触发撤消事件时创建重做动作,我将在ItemAdded 和 ItemRemoved LV 事件,但我一团糟,到处都是错误,没有任何东西可以按预期工作......好吧,你认为我是在浪费时间在这个逻辑上还是根本不是一个坏主意?自动取款机不工作
  • fiiiiiiiiuuuu... 最后我想我让 undomanager 使用新逻辑正常工作,在我的国家已经太晚了,所以明天我会用新代码更新,我想知道你的意见/suggestions 当我更新新代码时,也许你会在代码中看到一个错误来修复它,谢谢@Plutonix。
  • a) LV 撤消进入与所有其他控件不同的堆栈是否有(好的)理由?你的也会做标签编辑和复选框吗? b) 添加了“UnDoManager.UnDo”信息 c) 最好提出一个新问题
  • A) 我不明白,你的意思是我最好合并 undostack 和 redostack?是的,当然我会实现标签编辑和选中/取消选中复选框...我一步一步来... 太慢了。 C)真的,我不会再为此提出任何问题了......没有男孩可以编写完整的代码,当我通过几天写一些东西时,我需要再次完全重写它。没办法。
  • 你的LV为什么不画复选框?或者你把它藏在哪里了?我正在讨论如何使用一个 UnDoAction 来捕获 Text AND Check AND Item 更改作为练习。没有检查就很难调试。
猜你喜欢
  • 2013-11-14
  • 1970-01-01
  • 1970-01-01
  • 2012-07-16
  • 2010-09-30
  • 2014-04-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多