[How To] 如何编程实现保存WF 4工作流定义到图片

使用WF 4创建工作流的一大好处是图形化的设计器。也就是说在大多数情况下,开发人员不需要通过写代码的方式定义工作流,而是使用我们提供的设计器以图形化的方式创建出来。这不但大大提高了开发人员的生产力,更降低了设计流程的门槛以便非开发人员使用。图形化的另一个好处是直观、容易理解,因此在它几乎被所有工作流系统中。

举例来说,很多工作流系统中都有如下所述的典型应用场景:

  1. 业务人员使用工作定义一套业务流程,当然是以图形化的方式Winking smile
  2. 在使用这套业务流程的过程中,用户需要在系统的界面上(可能是桌面程序或网页)看到这个工作流的定义图,以便理解流程的逻辑。

为了实现这一应用场景,一个比较典型的做法是把工作流定义图保存成图片并显示给最终用户。最近在看MSDN WF 4论坛的时候,发现有一些开发人员不知道如何编程把WF 4工作流定义保存到图片,而有些客户也通过邮件向我提出同样的问题。今天我就来跟大家分享一下如何实现这个功能。

什么?难道WF 4默认不提供这一功能吗?还需要我自己写代码?有些朋友(特别是使用过WF 3.x的人)可能会发出这样的疑问。的确,WF 3.x中有一个很方便的API供你使用,而在WF 4中没有。主要原因是WF 3.x在图形方面使用的是WinForm而WF 4使用的WPF。WinForm支持在界面控件还没有被创建出来的时候生成图片,因此我们可以提供一个API来保存流程图。但是WPF却不支持这一点,也就是说,在WF 4中,你一定要先打开设计器(Workflow Designer),然后才能保存图片。正是由于这个约束,我们才无法实现保存图片的API。不过好在自己实现这一功能也比较简单,只不过有一些tricky的地方需要注意。我保证读完这篇文章,你就会觉得是小菜一碟儿了。

基本想法

实现保存图片功能的基本想法很简单:

  1. 新建一个工作流设计器(Workflow Designer)
  2. 打开需要保存图片的工作流定义
  3. 保存图片
  4. 关闭工作流设计器

其中第一、二步是必要的,因为就像我之前提到的WPF要求在创建出界面控件之后才能保存图片Sad smile。当然我们不需要打开Visual Studio,还记得WF 4提供一个rehostable workflow designer吗?对,我们就用它。关于如何创建一个rehostable workflow designer,请参考我们在MSDN上提供的示例代码。你也可以在这里下载到整套的WF 4示例。

写代码试试看

那么让我们写代码试试看。先来看看rehostable workflow designer的基本代码

 namespace Microsoft.Samples.DesignerRehosting
{
    public partial class RehostingWfDesigner : Window
    {
        private WorkflowDesigner workflowDesigner;

        public RehostingWfDesigner()
        {
            InitializeComponent();
        }

        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);

            // register metadata
            (new DesignerMetadata()).Register();
            // create a big workflow for saving image
            Activity workflow = new Sequence
            {
                Activities = { 
                    new Sequence(), new Sequence(), new Sequence(),
                    new Sequence(), new Sequence(), new Sequence(),
                    new Sequence(), new Sequence(), new Sequence()
                }
            };
            // create the workflow designer and load the workflow
            workflowDesigner = new WorkflowDesigner();
            workflowDesigner.Load(workflow);
            DesignerBorder.Child = workflowDesigner.View;
        }
    }
}

接着我们来看看如何在WPF中为界面控件生成一张图片:

 BitmapFrame CreateWorkflowDefinitionImage()
{
    const double DPI = 96.0;
    // this is the designer area we want to save
    Visual areaToSave = ((DesignerView)VisualTreeHelper.GetChild(
        this.workflowDesigner.View, 0)).RootDesigner;
    // get the size of the targeting area
    Rect size = VisualTreeHelper.GetDescendantBounds(areaToSave);
    RenderTargetBitmap bitmap = new RenderTargetBitmap((int)size.Width, (int)size.Height,
        DPI, DPI, PixelFormats.Pbgra32);
    bitmap.Render(areaToSave);
    return BitmapFrame.Create(bitmap);
}

这段代码比较直观,就是得到workflow designer中工作流图形的部分,然后使用WPF的一些API在内存中创建一张图片。接下来如果需要保存的一个文件的话,就比较简单了:

 void SaveImageToFile(string fileName, BitmapFrame image)
{
    using (FileStream fs = new FileStream(fileName, FileMode.Create))
    {
        BitmapEncoder encoder = new JpegBitmapEncoder();
        encoder.Frames.Add(BitmapFrame.Create(image));
        encoder.Save(fs);
        fs.Close();
    }
}

最后我们尝试在protected override void OnInitialized(EventArgs e)的最后调用以上两个函数保存图片:

 protected override void OnInitialized(EventArgs e)
{
    ...
    this.SaveImageToFile("test.jpg", this.CreateWorkflowDefinitionImage());
    Application.Current.Shutdown();

}

怎么会抛异常?

好,我们来运行以上代码!等等,为什么会有抛异常?

exception

稍微debug一下不难发现,原因是我们想要保存图片的区域的大小是负数。这是为什么呢?原来虽然我们是在打开workflow designer之后才保存的图片,但是在保存图片是,相应的界面控件并没有创建完毕,因此才得到了负数。这个问题要怎么解决呢?WPF中的每个控件都有一个LayoutUpdated事件,意思是控件已经创建并布局好了。然而仅仅简单的监听这个事件还不够,因为在完全创建好工作流定义图之前,控件会被布局很多次。因此,我们还要用一个计时器(Timer):

 private System.Timers.Timer timer;
private delegate void TimerDelegate();

protected override void OnInitialized(EventArgs e)
{
    this.timer = new System.Timers.Timer(1000);
    this.timer.AutoReset = false;
    this.timer.Elapsed += new ElapsedEventHandler(CaptureTimer_Elapsed);
    this.timer.Start();
    workflowDesigner.View.LayoutUpdated += new EventHandler(View_LayoutUpdated);
}

void CaptureTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    this.timer.Stop();
    this.timer = null;
    this.workflowDesigner.View.Dispatcher.BeginInvoke(
        System.Windows.Threading.DispatcherPriority.ApplicationIdle,
        (TimerDelegate)delegate()
        {
            this.SaveImageToFile("test.jpg", this.CreateWorkflowDefinitionImage());
            Application.Current.Shutdown();
        });
}

void View_LayoutUpdated(object sender, EventArgs e)
{
    if (this.timer != null)
    {
        this.timer.Stop();
        this.timer.Start();
    }
}

怎么保存不全?

好,我们再来运行试试。好!没有异常了!快去\bin\Debug下面看看保存的图片!哎?怎么有几个活动设计器不对呢?没有具体的活动设计器,只有个框而已。(为了节省版面,这里我把图片旋转了一下,实际上是垂直的没错,只是最后两个Sequence保存的有问题)

test

想要知道产生这个现象的原因,我们就需要讨论一下workflow designer中的一个高级机制叫做虚拟化(Virtualization) 。在这里,虚拟化的作用是:如果某些活动还没有出现在workflow designer中的可视区域内,我们就不去创建相关的活动设计器。这样做主要是为了提高workflow designer的性能。想象一下你的工作流中有上百个活动,但是在你的屏幕中一次只能看到其中十个。如果没有虚拟化,我们就不得不把所有上百个活动的设计器创建出来,这就会大大降低整个设计器的性能。而这里有问题的两个Sequence恰恰是被虚拟化的、还有没被创建出设计器的活动。

好了,原因说清楚了。那么怎么解决呢?道理是明摆着的,就是要让所有活动都出现在可视区域内。不过要怎么做呢?别急,我们的workflow designer提供了一个Fit to Screen功能,就是把整个工作流定义的图形缩放到在可视区域内完全显示。我们可以利用这一功能显示所有的活动。相应的代码加在哪里呢?对,void View_LayoutUpdated(object sender, EventArgs e)

 private bool IsVirtualized = false;

void View_LayoutUpdated(object sender, EventArgs e)
{
    if (!IsVirtualized)
    {
        ((RoutedCommand)DesignerView.FitToScreenCommand).Execute(null,
            this.workflowDesigner.Context.Services.GetService<DesignerView>());
        IsVirtualized = true;
    }
    if (this.timer != null)
    {
        this.timer.Stop();
        this.timer.Start();
    }
}

搞定!

最后我们再来运行一下。OK,这回图片正确啦!(同样,为了节省版面,这里我把图片旋转了一下)

test_final

最终版的代码可以在这里下载。希望以上讲解和示例能够给给我带来一些帮助。