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

首先推荐一下一个很不错的关于Workflow 4.0 的博客: The Activity Designer. 这个blog顾名思义介绍了很多与Workflow Designer设计开发相关的内容。而本文,或者说是从本文开始的一系列的文章,考虑将该博客中比较有意思的文章翻译成中文。 

最初被选定的是如何构建一个类似于Flowchart的free-form layout的工作流设计器。ICompositeView在MSDN上的文档并不多,而作者的这一系列的文章Freeform Custom Activity Designers using ICompositeView则很好的补充了官方文档的不足,那么闲话少说,就从这里开始吧:


Motivation: Building an activity like Flowchart

Creating composite activities and designers in Workflow Foundation 4.0 is easy when you use WorkflowItemPresenter and WorkflowItemsPresenter. While these controls are easy to use, there really are limits to what you can do with them. What are you supposed to do if you want to build a new free-form layout activity, like Flowchart?

In this article series we're going to use ICompositeView, which the framework exposes for this purpose, as a base to build an activity designer with some of the features of Flowchart, especially free-form layout. We start by inheriting ICompositeView and gradually we will learn how to provide a real implementation for the methods we inherit.

 

动机:构造一个与Flowchart相似的workflow

(这里给出MSDN中FlowchartFlowchart Designer的Link)

在Workflow Foundation 4.0中使用WorkflowItemPresenterWorkflowItemsPresenter构造复活活动与该活动的设计器是件容易的事情。尽管这些控件很容易上手,但它们确实有一些局限性。例如,你打算如何去实现一个和Flowchart那样,可以自由布局(free-from layout)的工作流活动设计器(workflow designer)呢?

在这一系列文章中,我打算使用.net framework提供的ICompositeView接口作为基础,在其之上构建一个有部分Flowchart功能的,特别是可以自由布局的工作流活动设计器。在设计过程中,我们让我们设计器首先需要继承该接口,然后一点一点的学习怎样实现其中的函数。

 


Setting up the project

Let’s start building our project in Visual Studio. It can be an Activity Library, or you can add these files into a rehosted designer project you have already set up. While we are coding the designer we will also need an activity class to test it on. Here is a skeletal activity class to use, with a couple attributes to make the XAML look nice. (Tip: the collection property always returns a non-null value, so it is readily bindable.)

 

建立项目

我们首先需要在Visual Studio中建立一个我们自己的项目。 一个方案是直接建立一个新的Workflow Activity Library项目。或者,把文件加入到一个已有的rehosted designer项目中去。在我们编写设计器之前,我们需要创建一个在测试我们的设计器时使用的活动(Activity),我们将该活动与设计器用DesignerAttribute(参见http://msdn.microsoft.com/en-us/library/system.componentmodel.designerattribute.aspx)相关联。请看下面的例子中的活动类的定义,该类所使用的类属性(attribute)-[ContentProperty()]可以使该活动在最终被序列化后的XAML好看一些。(小技巧:该类中的collection成员属性(property)总是会返回一个非空值,这样可以使绑定(binding)变得容易些)

1 CanvasActivity.cs:
2 [Designer(typeof(CanvasDesigner))]
3 [ContentProperty("Children")]
4 public sealed class CanvasActivity : NativeActivity
5 {
6 [DefaultValue(null)]
7 private Collection<Activity> _children;
8 public Collection<Activity> Children {
9 get
10 {
11 return (_children = _children ?? new Collection<Activity>());
12 }
13 }
14
15 protected override void Execute(NativeActivityContext context) {} //TODO
16   }

 

Next, we need WPF XAML for the custom designer. A canvas will probably be useful for letting us do free-form layout. And that’s about all we need.

 

下一步,我们需要用WPF XAML来实现该自定义设计器。Canvas控件应该可以达到我们的要求。以下就是该设计器需要的代码。

1 CanvasDesigner.xaml:
2
3  <sap:ActivityDesigner x:Class="ActivityLibrary1.CanvasDesigner"
4 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
5 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
6 xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation">
7 <Canvas Name="ContentCanvas" Width="500" Height="500" />
8  </sap:ActivityDesigner>

 

The real work will all happen in our C# code-beside file CanvasDesigner.xaml.cs. We start by declaring that our CanvasDesigner class implements ICompositeView. Right-click and choose ->'Implement Interface Explicitly'. Everything gets implemented as 'throw new NotImplementedException()'. We’ll compile it and then we can test the activity we created.

 

真正的程序逻辑会在在相应的代码隐藏页CanvasDesigner.xaml.cs中实现。首先,我们让CanvasDesigner继承ICompositeView并实现所有的接口。具体做法是:右键点击该接口名并选择“Implement Interface Explicitly”,Visual Studio会帮我们用“throw new NotImplementedException()”实现所有的接口。这样我们就可以编译并对它进行测试了。

 


Debugging the activity designer

Aside - We don’t need to be using a rehosted application to debug our designer, but if you have one there are advantages because it avoids symbol file read/write conflicts at build time. I didn’t use a rehosted application for testing. I just fired up a second instance of VS, and ‘attached process’ to the first instance of VS. This lets me see the output from Debug.WriteLine or set breakpoints. Use any similar technique to see when the interface methods get called. 


调试活动设计器 

题外话:rehosted程序在调试我们自己的活动设计器时会给我们带来一些好处,例如在编译设计器过程中避免了同时读/写符号文件(symbol file)所造成的冲突,但它并不是必须的。我的做法是,启动另一个VS的实例,将调试器附加到(attach)前一个VS的进程,这样可以让我看到Debug.WriteLine的输出或者设置断点。使用类似的技术来查看这些接口中的函数在什么时候被调用了。



Bug #1: We can’t paste

The results of debugging the bare bones interface implementation are actually disappointing. Most of the methods aren’t getting called at all. The one definite ‘hit’ on our code is when we cut an activity from elsewhere in our workflow, then right-click on our activity to pop up the context menu. The 'hit' is onICompositeView.CanPasteItems(). Since our code throws NotImplementedException we can't paste anything. Let's quick-fix that by implementing the function to return true:


Bug#1:不能粘贴活动

调试这些没有算不上真正实现的接口的过程令人相当的失望,大多数成员函数上的断点都没有被调用到。只有当我从workflow的某个地方剪切一个活动,并使用右键弹出上下文菜单时,ICompositeView.CanPasteItems()上所设置的断点被命中(hit)。既然当前我们的代码会抛出一个NotImplementedException的异常,我们当然不能粘贴任何东西到Canvas上。让我们将代码改成return true试试看:

1 bool ICompositeView.CanPasteItems(List<object> itemsToPaste)
2 {
3 return true;
4 }
5  

 

And rebuild...

重新编译…

 

Now we can select ‘Paste’ from the context menu, and ICompositeView.OnItemsPasted() gets called. Using the debugger we can inspect the parameters coming in. Let’s try to implement the function. I guessed that we need to do two things ourselves:

  • Add the pasted activities to CanvasActivity's children collection (in the model tree),
  • Create the activity designer visuals and add them to the visual tree

So how do we do that? Here’s the code to digest:

 

现在我们发现上下文菜单中“粘贴”菜单项变成了可选。选择“粘贴”项,我们发现ICompositeView.OnItemsPasted() 函数被调用。可以在调试器中看到该函数的参数值已经被传入进来。那么,让我们开始来实现这个函数。我猜我们有两件事需要去做:

  • 将被“粘贴”的活动加入到CanvasActivitiy的Children Collection中去,这是对model tree的更新 (关于Model Tree与后面要谈到的ModelItem,这里有一篇matt同志的文章给出了很好的解释。)
  • 添加相应的活动设计器到visual tree. 

那该怎么来做呢,请看下面的代码

1 void ICompositeView.OnItemsPasted(List<object> itemsToPaste, List<object> metadata,
2 Point pastePoint, WorkflowViewElementpastePointReference)
3 {
4 //itemsToPaste - some ModelItems, or maybe just raw CLR objects we need to paste.
5 //metadata - this could hold anything or nothing. Let’s leave it alone.
6
7 //itemsToPaste – ModelItem或者是CLR的对象,是我们需要粘贴到该活动的对象.
8 //metadata – 这个参数可能传入些什么东西或者什么都不传进来,先把它放在那里.
9  
10
11 ModelItem canvasActivity = this.ModelItem;
12 foreach(var i in itemsToPaste)
13 {
14 ModelItem createdModelItem = canvasActivity.Properties["Children"].Collection.Add(i);
15 var view = Context.Services.GetService<ViewService>().GetView(createdModelItem);
16 this.ContentCanvas.Children.Add((UIElement)view);
17 }
18 }
19  

 

Explanatory Points:

  • The object we add to the model tree might not be a ModelItem yet. It would be automatically wrapped in a ModelItem when we add it though. In this case we get the wrapper ModelItem from the return value of Add().
  • We don’t new an activity designer. We instead request ViewService to create it for us. ViewService is the service that figures out which designer class to instantiate automatically, based on DesignerAttribute.

Testing our implementation, success – we can now paste an activity!

 

我们发现:

  • 该被加入到model tree的对象可能并不是一个ModelItem,但当我们调用Add()函数将该队将加入到Model Tree以后,他会被自动的包装成一个ModelItem,并作为Add()函数的返回值。
  • 我们并不需根据传入对象的类型自己去构造一个相对应的活动设计器(Activity Designer),ViewService(MSDN的Link) 可以帮助我们完成以上的工作。ViewService会根据该对象所具有的DesignerAttribute自动构造相应的设计器实例。

测试一下我们的实现,成功!我们现在可以粘贴一个活动到我们自己的设计器上了。

 

Bug #2: Reloading pasted activity from XAML doesn’t work

Our pasted activity gets saved out to XAML properly, which is important. But when we reload the XAML file, we can’t see the activity we created.

We need to populate the Canvas when our designer is created and initialized with the activities loaded from XAML. We can’t do this by overriding OnInitialized(), because this.ModelItem may still be null. The best place I can think of to do this is when overriding OnModelItemChanged().

 

Bug#2:不能重新从XAML文件中加载粘贴的活动

我们可以看到那些被粘贴的活动被正确的写入了XAML文件,但是当我们关闭并重新加载该XAML文件后,我们发现这些活动并没有显示在我们自己的设计器中。
 我们必须在我们自己的设计器被加载的时候,根据XAML中已有的活动来构造我们的Canvas。但重载OnInitialize()是行不通的,因为在那个时候,this.ModelItem为空。所以,重载OnModelItemChanged()是我认为最佳的选择。

1 protected override void OnModelItemChanged(object newValue)
2 {
3 this.ContentCanvas.Children.Clear();
4 ModelItem canvasActivity = (ModelItem)newValue;
5 foreach (ModelItem modelItem incanvasActivity.Properties["Children"].Collection)
6 {
7 var view = Context.Services.GetService<ViewService>().GetView(modelItem);
8 this.ContentCanvas.Children.Add((UIElement)view);
9 }
10 }

 

This is very similar to what we did above. Bug Fixed. Next bug?

这些代码和我们前面实现粘贴功能的代码很像,看来Bug被修好了。下一个Bug?

 

Bug #3: We can’t delete the activity we added by using paste

We can add items now, using Paste. So delete should be really easy, right? Let's select something in our activity, and press [DEL]. Uh oh, we're not hitting any breakpoints ICompositeView.OnItemsDelete() just never gets called. Why not?

This is actually a horrible puzzle to figure out. It's easy finding out why you are getting called, compared to figuring out why you are not. In retrospect, I should have tried thinking about it from the caller’s point of view.

Suppose I’m an event handler for [DEL]. I have to call OnItemsDelete(). But the only information I have to work on is probably the current selection which is the activity being deleted, not the containing activity. So how should I figure out who the containing activity is? This should be easy, but it’s not… maybe if someone could add piece of data on the view of the contained activity saying who its container is my life would be easier?

OK, honestly I wouldn’t have imagined that without peeking at the framework implementation, that is the story I made up after understanding. But yes, the framework is expecting that every place we add a WorkflowViewElement as a containee of our CanvasActivity, we do a little extra work by calling DragDropHelper.SetCompositeView(). This creates the missing piece of data.

 

Bug #3:无法删除被粘贴过来的活动

既然我们现在已经可以通过粘贴添加新的活动,那么删除它们也应该是很容易的事情,对吧?我们首先选中一些活动,然后按下[DEL]键。恩……好像ICompositeView.OnItemsDelete() 并没有被调用,为什么呢?

这应该算是个大难题,因为与找出一个函数为什么会被调用相比,找到一个函数为什么没有被调用很有难度。在总结过前面的问题之后,我想应该试着从一个函数调用者的角度来考虑这个问题。
假设我是[DEL]的事件处理函数(event handler),那么应该由我去调用OnItemsDelete()。但是,我仅仅知道当前那个活动需要被删除是不够的,因为我还需要知道谁包含了(containing)这个活动。那么事件处理函数怎么才能去找出到底谁是那个容器活动(containing activity)呢?看起来很简单,似乎不是这样……如果谁能在向容器活动添加新的活动时,把谁是的容器信息添加进来,那我作为时间处理函数在处理事件时不就轻松多了吗?
好吧,我承认如果没有偷看framework的实现,我并没能想明白这到底是怎么一回事情,至于那个故事,是我在弄清楚来龙去脉以后杜撰出来的。不过,事实是framework的确要求在添加WorkflowViewElement的时候将CanvasActivity做为容器信息一并加入,我们可以使用DragDropHelper.SetCompositeView()这个函数来设置两者的关系。下面的代码将这些信息加入进来。

1 this.ContentCanvas.Children.Add((UIElement)view);
2 DragDropHelper.SetCompositeView((WorkflowViewElement)view, this);
3  

 

 

Adding the crucial line of code is needed in two places since we have two functions that add children to our CanvasActivity.
OK. Now we know how to get OnItemsDelete() called, we'll discuss how to implement OnItemsDelete() in Part 2 and Part 3.

 

既然我们有两个函数会涉及到添加新的子活动到CanvasActivity(OnModelItemChanged和OnItemsPasted),那么每一个函数都需要添加这两行关键的代码。
好了,到此为止,我们知道了怎样使OnItemsDelete()被调用到,我会继续在本系列的第二第三部分讨论如果实现OnItemsDelete().

posted @ 2010-05-31 22:47  telescope  阅读(486)  评论(0编辑  收藏  举报