[译]使用ICompositeView实现自由布局的工作流设计器 III
In Part 1 we started implementing a freeformlayout designer using ICompositeView. So far we have implemented ICompositeView.OnItemsPasted() and ICompositeView.OnItemsDelete(). But the bad news is that our implementationsaren’t keepers. The reason is that they don’t support Undo and Redo.
从本系列的第一部分开始,我们使用ICompositeView来实现一个可自由布局的设计器。到目前为止,我们已经实现了ICompositeView.OnItemsPasted() 和ICompositeView.OnItemsDelete()这两个函数。但是我们并没有完成所有的实现,原因是它们并不支持undo和redo。
The problem scenario
-
- Add an If activity tothe workflow
- 'Cut' the activity(Ctrl+X)
- Add a CanvasActivityto the workflow
- 'Paste' the Ifactivity inside the CanvasActivity (Ctrl+V)
- 'Delete' the Ifactivity ([DEL])
- Undo (Ctrl+Z)
Expected: The visual of the If activityshould reappear. (Also, but less importantly, the CanvasActivity should show avalidation error: 'One or more children have validation errors or warnings'.)
Observed: The visual for the If activity doesnot appear. But the validation error does appear.
会造成问题的情况
-
- 添加一个If活动到工作流
- 剪切这个活动(Ctrl+X)
- 添加CanvasActivity到工作流
- 粘贴这个If活动到CanvasActivity(Ctrl+V)
- 删除这个If活动([DEL])
- Undo(Ctrl+Z)
希望的结果:If活动应该重新出现。(这时CanvasActivity应该还会显示一个验证错误: 'One or more children have validation errorsor warnings',当然,这并不是那么重要)
观察到:If活动并没有重新出现,但是却多了一个验证错误。
The code again
Here is how we implemented ICompositeView.OnItemsDelete() last time.
重新回顾一下代码
这是上一篇中我们对ICompositeView.OnItemsDelete()的实现
1 void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)
2 {
3 ModelItem canvasActivity = ModelItem;
4 foreach (var i in itemsToDelete)
5 {
6 UIElement view =uot;> (UIElement)i.View;
7 canvasActivity.Properties["Children"].Collection.Remove(i);
8 this.ContentCanvas.Children.Remove(view);
9 }
10 }
Understandingwhat went wrong
Because we never coded any explicit supportfor undo, so it’s no surprise that it doesn’t quite work in ourscenario. What might be a surprise is that we have undo half-workingalready! How?
The code in OnItemsDelete() creates thecorrect state of the View tree and the Model tree. And when we press Ctrl+Z wecan see that the Model tree change is being undone (we can also verify bysaving to XAML). But again, how?
Well, how does the Workflow Designerimplement undo/redo? Last time we learned about howModelEditingScope isused to control the grouping of actions on the Undo stack. What we sort ofbrushed over is that for each call to ModelProperty.SetValue() orModelItemCollection.Add(), an item is being created on the undo stack for usautomatically. (Gory implementation details to be in a later post).
Why doesn’t the Workflow Designer also knowhow to undo our changes to the View? Why should we expect it to? Good reasonsnot to expect it are that it’s not performant nor what the user wants. Imaginethese scenarios:
1) Zoom in to view a single activity. If theactivity was collapsed, we need to populate the views of its children. We alsoneed to hide or delete all the views which are no longer ‘in view’.
2) How many times do I press undo? Should undorevert through every single view you used? It would become impossible toremember how many times you need to press Ctrl+Z to undo the last actual edit thatyou made.
Now I’ve got the good argument out of the wayhere’s a bad argument I find much more appealing: It’s not the WPF way! TheWPF way is to use binding (and change notifications) to update the viewautomatically in response to changes in the model (or view model - MVVM). Thinkabout the humble check box. It doesn’t need to know about a global undo/redostack if we just bind it to the appropriate model data which is itself coveredwith an undo/redo stack.
弄清楚那里出了问题
因为我们并没有显示的写过任何代码去支持undo,因此不能正确的undo也不是件令人惊奇的事情。相反令人惊奇的事情在于,undo已经有一半的功能是正确的了!怎么回事?
OnItemsDelete()中的代码正确的修改了Visualtree和Model tree,当我们按下Ctrl+Z之后我们发现Modeltree被正确的恢复了(这一点,可以通过被保存下来的XAML文件看出来).但是,还是刚刚那个问题,这是怎么一回事情呢?
工作流设计器(WorkflowDesigner)是如何实现undo/redo这一功能的呢?在上一篇文章中,我们学到了ModelEditingScope是如何把那些操作聚合成一个(原子)操作并放到Undo栈上。但我们并没有重视的是:在每一次调用ModelProperty.SetValue()或者ModelItemCollection.Add()时,undo栈上都会自动的加上相应的ModelItem操作单元。(实现细节会在后面的文章中进行剖析)
那么为什么工作流设计器并不知道如何去undo相关的视图呢?我们为什么希望工作流设计器能够这么做?解释这个问题的合理的理由是:这个不好实现而且也并不是用户所期望的。让我们来看看下面这些情况:
1) 当我们将某个活动设计器放大,如果这个设计器被合上了(collapsed),那么我们需要重新绘制他们的孩子节点。我们还需要隐藏或者删除所有的在视图中不可见的视图元素。
2)那么我们需要按多少键去undo这个视图上的变化?是不是要将每一个被使用到的视图元素都恢复一遍?要记得回到上次你做完的修改需要按多少次Ctrl+Z是件不太可能的事情。
还有一个比上面的例子更加令人信服的理由:WPF不是这样做的!标准WPF的方式是使用binding(或者变更通知机制changenotification)根据模型(或者是viewmodel-MVVM)中的变化来自动更新视图的。让我们来看看最简单的check box控件,如果我们已经将它与它对应的模型中的数据(modeldate)绑定起来,它并不需要知道全局undo/redo栈中的内容因为这个控件已经(通过绑定)被这个undo/redo栈中的某个项所包含了。
Model Change Notifications
Actually the main point of the WorkflowDesigner’s model tree is to provide these change notifications and enable thebinding of view to model. Unfortunately, Canvas.Children isn't bindable, andeven if it were, I don’t see how it would handle layout, so we are just goingto have to solve our problem in code. First, we’re going to modify theOnModelItemChanged override we wrote in Part1.
模型更变通知机制
实际上,工作流设计器的modeltree需要提供一种变更通知的机制,以及使得视图可以被绑定到相应的对象的机制。不幸的是,Canvas.Children是不能被绑定的,即使它可以,我也不太清楚任何让它来处理视图的布局。因此,我们不得不在代码中解决以上的问题。首先,我们打算修改一下我们子文章第一部分重写的OnModelIemChanged函数。
1 protected override void OnModelItemChanged(object newValue)
2 {
3 ModelItem canvasActivity = (ModelItem)newValue;
4 Update(canvasActivity);
5 canvasActivity.Properties["Children"].Collection.CollectionChanged +=
6 new System.Collections.Specialized.NotifyCollectionChangedEventHandler
7 ((senders, args) => Update(this.ModelItem));
8 }
The important part is that we are going tolisten for notifications to changes to CanvasActivity.Children, and respond tothem by calling Update(ModelItem):
最重要的部分是我们需要监听CanvasActivity.Children的变化,然后在被通知后调用Update(ModelItem)
1 void Update(ModelItem canvasActivity)
2 {
3 this.ContentCanvas.Children.Clear();
4 foreach (ModelItem modelItem incanvasActivity.Properties["Children"].Collection)
5 {
6 var view = Context.Services.GetService<ViewService>().GetView(modelItem);
7 this.ContentCanvas.Children.Add((UIElement)view);
8 DragDropHelper.SetCompositeView((WorkflowViewElement)view, this);
9 }
10 }
11
Once we have the change notification/handlingin place, we should rewrite OnItemsDelete() andOnItemsPaste():
当我们完成了这些修改后,我们需要重新写一下OnItemsDelete()和OnItemsPaste():
1 void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)
2 {
3 ModelItem canvasActivity = ModelItem;
4 foreach (var i in itemsToDelete)
5 {
6 canvasActivity.Properties["Children"].Collection.Remove(i);
7 }
8 }
9 void ICompositeView.OnItemsPasted(List<object> itemsToPaste,
10 List<object> metadata, Point pastePoint, WorkflowViewElementpastePointReference)
11 {
12 System.Activities.Presentation.Model.ModelItem canvasActivity =this.ModelItem;
13 foreach (var i in itemsToPaste)
14 {
15 canvasActivity.Properties["Children"].Collection.Add(i);
16 }
17 }
18
In these functions all we should do is updatethe model. And then we can undo delete, hurray!
在上面的这些代码中,我们需要做的就是更新模型(model)。加油,我们已经可以进行删除操作undo了。