[译]使用ICompositeView实现自由布局的工作流设计器 II

Last time we found out that ICompositeView.OnItemsDelete() will never get called - unless we tell our contained activities exactly which composite view they belong to. But we didn’t get to implement OnItemsDelete(). Our entire article today is filled with attempts to implement OnItemsDelete(). Unfortunately the first several implementations I try won’t work. If you are here just looking for the right approach, and not interested in the learning curve, you might want to jump to Part 3.

 

接上回书,在上一篇文章中我们发现ICompositeView.OnItemsDelete() 只有在被包含的活动知道自己属于哪个Composite View情况下才会被调用。但是,我们并没有实现OnItemsDelete()函数。本文通篇描述了我为了实现OnItemsDelete()函数时所做的尝试。不幸的是,最初的几个实现都不能正确的工作。如果你仅仅希望知道如何正确的实现,并对这条学习曲线没有兴趣的话,请你直接跳到本系列的第三部分

 

Implementing ICompositeView.OnItemsDelete

实现ICompositeView.OnItemsDelete

Attempt #1:

尝试#1

 

1 void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)
2 {
3 ModelItem canvasActivity = this.ModelItem;
4 foreach (var i in itemsToDelete)
5 {
6 var view = (UIElement)Context.Services.GetService<ViewService>().Getiew(i);
7 ContentCanvas.Children.Remove(view);
8 canvasActivity.Properties["Children"].Collection.Remove(i);
9 }
10 }

 

What’s wrong with this code?
When you press [DEL] to delete your activity you will see (by looking at generated XAML) that the model tree item removal works, so the function is half right. What goes wrong? The visual tree update doesn’t occur, i.e. the activity’s visual does not get deleted from our designer's canvas. Because I am a WPF newbie I assumed this was somehow due to me being a stupid user: "Probably there is some good reason Canvas doesn't want me removing its children. Blah. Stupid me." We will see later this is wrong.

 

上面这段代码有什么问题么?
当[DEL]被按下的时候,你会发现Model tree中的活动被正确的删除了(可以通过在删除后查看序列化的XAML文件发现),因此这个函数实现只做对了一半。那什么问题造成visual tree的更新操作并没有发生,即相关的设计器并没有从canvas上删除呢?因为我是个WPF入门级选手,我觉得我的问题是因为我对WPF无知所造成的,我因此认为:“可能是Canvas因为某种原因并不想让我删除这些孩子节点。哇,好笨啊。”不过在后来我们会发现,其实是我想错了。

 

Attempt #2:

尝试#2

1 void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)
2 {
3 ModelItem canvasActivity = this.ModelItem;
4 foreach (var i in itemsToDelete)
5 {
6 canvasActivity.Properties["Children"].Collection.Remove(i);
7 }
8 this.OnModelItemChanged(this.ModelItem); //repopulate canvas (see below for details)
9   }
10  

  

I was going to refactor that OnModelItemsChanged() call into something nicer, but I had another problem. This code didn't work either! What is going on?
To see what was going on, I adding Debug.WriteLine() all through my code and attaching a debugger: 

我打算把那句对OnModelItemsChanged()的调用重构成一个新看起来不那么难看的函数,但是我发现另一个问题,这段代码也不工作!到底怎么了?
为了弄明白发生了什么情况,我在我的代码中添加了Debug.WriteLine()并附加(attach)了一个调试器:

1 protected override void OnModelItemChanged(object newValue)
2 {
3 Debug.WriteLine("OnModelItemChanged");
4 ModelItem canvasActivity = (ModelItem)newValue;
5 this.ContentCanvas.Children.Clear();
6 foreach (ModelItem modelItem incanvasActivity.Properties["Children"].Collection)
7 {
8 Debug.WriteLine("modelItem: {0}", GetID(modelItem));
9 var view = (UIElement)Context.Services.GetService<ViewService>().GetView(modelItem);
10 Debug.WriteLine("view: {0}", GetID(view));
11 this.ContentCanvas.Children.Add((UIElement)view);
12 DragDropHelper.SetCompositeView((WorkflowViewElement)view, this);
13 }
14 }
15  

 

The results of this exercise:

以下是我实验的输出

 

(*I Open the XAML file*)
OnModelItemChanged
modelItem: 0
view: 0

(*I Press Delete*)
OnModelItemChanged
modelItem: 0
view: 0

 

Wait, there's still a model item in the Children collection? That’s weird. Look at the XAML we get when we save! No children! 

等等,为什么还有一个model item在Children collection中?(作者指当Delete被调用后仍然可以foreach这个collection),这的确很奇怪,打开XAML我们会发现,该CanvasActivity并不包含任何子活动

 

    <local:CanvasActivity sap:VirtualizedContainerService.HintSize="514,536" />

 

How can this be possible?

这怎么可能呢?

Meet the ModelEditingScope 

What is a ModelEditingScope? Let's look at an example (sans pretty colors).

 

关于ModelEditingScope 

那什么是ModelEditingScope呢?然我们看看下面的代码

 

1 private void CombinedEdit(ModelItem canvasActivity)
2 {
3 using (ModelEditingScope edit = canvasActivity.BeginEdit())
4 {
5 canvasActivity.Properties["Children"].Collection.Add(new If());
6 canvasActivity.Properties["Children"].Collection.Add(new If());
7 edit.Complete();
8 }
9 }

 

This code does adds two If activities as children of a CanvasActivity. What's special about this code is that the two add actions happen as a single (atomic) action. It is atomic both from the point of view of undo/redo, and error recovery if an exception is thrown. Time for a comparison table: 

这段代码将两个If活动作为孩子活动添加到了CanvasActivity。这段代码特殊之处在于它将这两个操作看成了一个单一的(原子的) 操作。不论是从Undo/Redo的角度,还是说在修改过程中遇到异常需要自动恢复这一角度,这两个操作都是原子操作。请看如下的表格: 

 

 

With Editing Scope

Without Editing Scope

Undo Stack

1 Undo Item

2 Undo Items

Single Commit
of side effects

No

Yes

 

When we add the using (ModelEditingScope clause as above, then the Workflow Designer's undo/redo stack only increases in size by one. So when we press Ctrl+Z both of the 'adds' get undone at one instant.
The other important benefit of ModelEditingScope is error recovery. Suppose we use (using) a ModelEditingScope and half-way through the using statement an exception is thrown. Then we never reach line 5 of the above code: edit.Complete(); which is the line that commits the editing scope and all of its changes. When the exception exits the using block, ModelEditingScope.Dispose() is automatically called, and all the uncommitted changes to the model tree are thrown away.
Look at our problem scenario and the debugging output above, and now we start to understand what is going on. OnItemsDelete() is actually being called from inside of a ModelEditingScope created by the command-handling code in System.Activities.Presentation. Even though we called Remove() on the model item collection, our debugging code still shows us that the item is in the collection because the editing scope hasn’t been committed yet. After clearing the canvas, oops! We add a child view corresponding to a ModelItem that is in a state of limbo – not yet dead, not yet alive. Mystery solved.
Solving the mystery is good news, but I still don't have a working OnItemsDelete() function and it seems like an attempt to write code working around these heisen-ModelItems that may or may not be alive would be messy. Does Canvas really not support Children.Remove()? Can we instead fix implementation attempt #1?

 

当我们使用了上面代码中的using(ModelEditingScope…代码块,那么在这using作用域范围内的Workflow Designer所做的所有操作,Undo/Redo堆栈都只会有一个单位的变化。因此,当我们按下Ctrl+Z时,这两个“添加”操作只需要一步就能undo.
另一个可以从ModelEditingScope中受益的功能是从错误恢复。假设我们在该ModelEditingScope所在的using块中有一个异常被抛出。那么上面代码中的第5行:edit.Complete(),也就是用于提交确认当前作用域中所有更新的代码将不会被执行。当需要从该using块中退出时,ModelEditingScope.Dispose()函数将会被自动调用,所有未被提交确认的修改都将从model tree中被删除。在仔细分析了我们出问题的情况以及调试的输出信息之后,我们现在应该开始明白到底出了什么问题。OnItemsDelete()其实是在一个由System.Activities.Presentations中的命令处理(command-handling)代码创建的ModelEditingScope中被调用的。尽管model item集合的Remove()函数被调用,我们的调试代码打印仍然会打印出那个被删除的项仍然没有被删除,因为在此时作用域仍没有被提交确认。所以,当我们从canvas上删除了一个活动之后,一不小心,我们又将那个已经被删除的半死不活的ModelItem以及其相应的视图重新添加到集合中去。恩~,问题解决了。
解决刚刚那个问题是个好消息,但是,我们并没有一个可以工作的OnItemsDelete()函数的实现。而且,似乎要处理好这些不知它们死活的ModelItems非常麻烦,这样很容易造成混乱。到底Canvas能不能支持Children.Remove()呢?我们能不能稍微修改一下尝试#1中的代码呢?

 Getting Delete Working

A simple plain WPF experiment proved to me that Canvas does support Children.Remove(). (This restored my faith in WPF a little!) So the real reason our first implementation of OnItemsDelete() doesn't work was actually something else, let's look at it again:

 

让删除正常的工作

我用一个简单的WPF实现程序证明了Canvas类确实支持Children.Remove()函数。(这让我对WPF的信心有所恢复。)因此,导致我们一开始实现的OnItemsDelete()不能正常工作的罪魁祸首应该是其他的问题,让我们来重新看看代码: 

1 void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)
2 {
3 ModelItem canvasActivity = this.ModelItem;
4 foreach (var i in itemsToDelete)
5 {
6 var view = (UIElement)Context.Services.GetService<ViewService>().Getiew(i);
7 ContentCanvas.Children.Remove(view);
8 canvasActivity.Properties["Children"].Collection.Remove(i);
9 }
10 }
11  

 

If we add Debug.WriteLine() statements everywhere, we can find out that the number of items in ContentCanvas.Children doesn't actually change when we execute line 5. We tried to remove something from the collection that isn't in the collection. Why?
The reason is simply that ViewService.GetView() doesn't do what I thought it did. It actually seems to be creating a new view every time I call it. (I was expecting it would be smarter, caching views, and return the existing one where possible.)
So what is the way to get a view for our ModelItem that already exists? Hmm, ModelItem has a propertyView which returns a DependencyObject… That seems like questionable architecture. But convenient. Finally, we fix our implementation of OnItemsDelete.

 

如果我在代码中多加些Debug.WriteLine()函数,我们会发现当代码line 5在执行之后,ContentCanvas.Children的个数并没有改变。也许是我们要删除的东西并不在集合之中,为什么会这样子呢?
原因很简单,ViewService.GetView()并没有像我所想的那样工作。事实上,当该函数被调用时,它总是会返回给我一个新的视图(view)。(我本来以为这东西该聪明点,它应该知道缓存已有的视图,并且当缓存中存在已有的视图时,它会直接返回缓存的内容)
如果那个ModelItem的视图已经存在,我们应该怎么才能得到它呢?恩~,ModelItem有一个View属性会返回一个DependencyObject…这个构架好像有点问题,但至少很方便。最终,我们修改并完成了我们的OnItemsDelete实现。

 

Attempt #3:

尝试#3

1 void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)
2 {
3 ModelItem canvasActivity = ModelItem;
4 foreach (var i in itemsToDelete)
5 {
6 UIElement view = (UIElement)i.View;
7 canvasActivity.Properties["Children"].Collection.Remove(i);
8 this.ContentCanvas.Children.Remove(view);
9 //not sure yet if we should try this:
10 //DragDropHelper.SetCompositeView((WorkflowViewElement)view, null);
11   }
12 }
13  

 

I’m still going to call this one just an attempt, because while we can now successfully delete an Activity, it turns out there are other issues with the implementation. Read on to Part 3.

我依然认为这仅仅只是一种尝试,因为即使我们已经可以把活动删除,但实现中仍然存在其他问题。请听第三回分解

 

posted @ 2010-06-01 16:33  telescope  阅读(374)  评论(0编辑  收藏  举报