[T2-401 デモ (2)] Rehosting を使ったエンドユーザへのワークフロー公開
こんにちは。
- WCF の Transport レベルの Custom Channel Sample
- Rehosting を使ったエンドユーザへのワークフロー公開
- WF による Rule Base のアプリケーション
- Custom の SharePoint Workflow Editor
製品開発者向けということで実施した Tech Ed の T2-401 セッションですが、お約束通り、デモを添付しておきます。
ここからは WF でお見せしたデモです。
まずは、WF のリホスティングによるワークフロー公開とエンドユーザーによるワークフロー作成のデモで使用したサンプルプロジェクトのダウンロードを掲載しておきます。
download sample
ワークフローの公開には、いろいろと実装方法の選択肢と、それぞれのメリット・デメリットがありましたが、ここでの説明は割愛します。
ここでは、 WorkflowView を使用したリホスティングを使用しています。
コードは以下の通りです。
ワークフローデザイナーへの xoml のワークフローのロードをおこないますが、まず、WorkflowView を使った場合には、ロードやアンロードの処理のために、必ず Workflow Loader が必要でした。デザイナー上の「アイテム選択」、「コンテキストメニュー選択」、などの機能は、「サービス」として追加 (AddService) をおこないます。(下記で、WorkflowMenuCommandService はカスタムで作成したメニュー表示のためのクラスであり、後述でそのクラスを実装しています。)
さらに WorkflowView でデザイナーリホスティングをする場合の面倒なもう 1 つのポイントとして、Validation Check (Visual Studio のデザイナー上で出てくる赤のスマートタグアイコンです) へ対処しなければならないという点がありました。この Validation Check を意識して参照すべきアセンブリの設定などをおこなっておく必要があるため、以下ではそうした設定をおこなっています。
private DesignSurface designSurface;
private WorkflowView workflowView;
private MyWorkflowLoader loader;
string projectpath;
public Form1()
{
InitializeComponent();
// ワークフロープロジェクトのパスを取得
projectpath = Path.Combine(Directory.GetCurrentDirectory(), @"..\..\..\CustomWorkflowLibrary");
// ワークフローデザイナーの初期化
this.designSurface = new DesignSurface();
loader = new MyWorkflowLoader(Path.Combine(projectpath, "Workflow1.xoml"));
designSurface.BeginLoad(loader);
this.workflowView = new WorkflowView((IServiceProvider) this.designSurface);
splitContainer1.Panel1.Controls.Add(this.workflowView);
this.workflowView.Dock = DockStyle.Fill;
IDesignerHost designerHost = (IDesignerHost)designSurface.GetService(typeof(IDesignerHost));
designerHost.Activate();
// コンテキストメニューを使用可にする
//(下記で定義するカスタムのメニューを設定)
IMenuCommandService menuService = new WorkflowMenuCommandService((IServiceProvider)workflowView);
designerHost.AddService(typeof(IMenuCommandService), menuService);
// 参照するアセンブリ(一部) を設定
// (パラメータのValidation チェックなどの際に、このアセンブリを見にいくため)
TypeProvider typeProvider = new TypeProvider((IServiceProvider)workflowView);
typeProvider.AddAssemblyReference(@"..\..\..\CustomWorkflowLibrary\bin\Debug\CustomWorkflowLibrary.dll");
designerHost.AddService(typeof(ITypeProvider), typeProvider);
// 以下、省略
. . .
}
// ワークフローローダーのクラス
class
MyWorkflowLoader : WorkflowDesignerLoader
{
string xomlfile;
public MyWorkflowLoader(string filename)
{
xomlfile = filename;
}
public override TextReader GetFileReader(string filePath)
{
throw new NotImplementedException();
}
public override TextWriter GetFileWriter(string filePath)
{
throw new NotImplementedException();
}
public override string FileName
{
get { return xomlfile; }
}
// ここは、ロード時に呼ばれます
protected override void PerformLoad(IDesignerSerializationManager serializationManager)
{
base.PerformLoad(serializationManager);
XmlReader reader = new XmlTextReader(this.xomlfile);
WorkflowMarkupSerializer xomlSerializer = new WorkflowMarkupSerializer();
Activity rootActivity = (Activity)xomlSerializer.Deserialize(reader);
reader.Close();
NestedActivityLoad(rootActivity);
}
// ネストされるすべてのアクティビティをデザイナーにロード
protected void NestedActivityLoad(Activity root)
{
IDesignerHost designerHost = (IDesignerHost)GetService(typeof(IDesignerHost));
designerHost.Container.Add(root);
if (!(root is CompositeActivity))
return;
// 再帰呼び出し
foreach (Activity nested in ((CompositeActivity)root).Activities)
NestedActivityLoad(nested);
}
// ここは、アンロード(保存) 時に呼ばれます
public override void Flush()
{
IDesignerHost host = (IDesignerHost)GetService(typeof(IDesignerHost));
Activity rootActivity = (Activity)host.RootComponent;
XmlTextWriter xmlWriter = new XmlTextWriter(this.xomlfile, Encoding.Default);
WorkflowMarkupSerializer xomlSerializer = new WorkflowMarkupSerializer();
xomlSerializer.Serialize(xmlWriter, rootActivity);
xmlWriter.Close();
}
}
// カスタムのコンテキストメニューのクラス (上述)
internal sealed class WorkflowMenuCommandService : MenuCommandService
{
public WorkflowMenuCommandService(IServiceProvider serviceProvider)
: base(serviceProvider) { }
public override void ShowContextMenu(CommandID menuID, int x, int y)
{
// アイテムのショートカットが選択された場合
if (menuID == WorkflowMenuCommands.SelectionMenu)
{
// (複数選択の場合もあるが、最初しかみない. . .)
ISelectionService selectionService = (ISelectionService)GetService(typeof(ISelectionService));
object[] selection = new object[selectionService.SelectionCount];
selectionService.GetSelectedComponents().CopyTo(selection, 0);
if ((selection[0] is CustomActivityLibrary.Add3Activity) || (selection[0] is CustomActivityLibrary.Multiple3Activity))
{
MenuItem menuItem;
ContextMenu contextMenu = new ContextMenu();
menuItem = new MenuItem("名前の変更", new EventHandler(OnMenuClicked));
menuItem.Tag = selection[0];
contextMenu.MenuItems.Add(menuItem);
menuItem = new MenuItem("削除", new EventHandler(OnMenuClicked));
menuItem.Tag = selection[0];
contextMenu.MenuItems.Add(menuItem);
WorkflowView workflowView = (WorkflowView)GetService(typeof(WorkflowView));
contextMenu.Show(workflowView, workflowView.PointToClient(new Point(x, y)));
}
}
// 今回省略しますが、Validation Error の場合のコンテキストメニューの処理も、
// ここに記載します(menuID はWorkflowMenuCommands.DesignerActionsMenu が来ます!)
// . . .
}
private void OnMenuClicked(object sender, EventArgs e)
{
MenuItem menuItem = (MenuItem)sender;
if (menuItem.Text == "名前の変更")
{
Activity act = (Activity) menuItem.Tag;
PropertyDescriptor propertyDescriptor = TypeDescriptor.GetProperties(act)["Name"];
propertyDescriptor.SetValue(act, "テスト");
}
else if (menuItem.Text == "削除")
{
object o = menuItem.Tag;
WorkflowView workflowView = (WorkflowView)GetService(typeof(WorkflowView));
IServiceProvider provider = (IServiceProvider)workflowView;
IDesignerHost designerHost = (IDesignerHost)provider.GetService(typeof(IDesignerHost));
CompositeActivity parentActivity = (CompositeActivity)((Activity)o).Parent;
parentActivity.Activities.Remove((Activity)o);
designerHost.RootComponent.Site.Container.Remove((IComponent)o);
}
}
}
このサンプルで、アクティビティの追加ボタンを押した際のコード (デザイナー上にアクティビティを設定するコード) は以下になります。
普通に、コードからワークフローを設定するのとほぼ同等に処理をおこなうスタイルで記述できます。
private void addBtn_Click(object sender, EventArgs e)
{
// アクティビティの追加
object o = Activator.CreateInstance(Type.GetType("CustomActivityLibrary.Add3Activity, CustomActivityLibrary"));
IServiceProvider provider = (IServiceProvider) workflowView;
IDesignerHost designerHost = (IDesignerHost) provider.GetService(typeof(IDesignerHost));
SequentialWorkflowActivity rootActivity = (SequentialWorkflowActivity)designerHost.RootComponent;
((CompositeActivity) rootActivity.Activities[0]).Activities.Add((Activity) o);
designerHost.RootComponent.Site.Container.Add((IComponent) o);
// プロパティの設定
Activity activity = (Activity)o;
PropertyDescriptor propertyDescriptor = TypeDescriptor.GetProperties(activity)["CalcValueProperty"];
ActivityBind bind = new ActivityBind();
bind.Name = "Workflow1";
bind.Path = "paramValue";
propertyDescriptor.SetValue(activity, bind);
}
最後に、コンパイルをおこなって配置をおこなうコードが以下です。
ローダーの Flush コマンドにより、上述の WorkflowLoader のコードの中で実施していた WorkflowMarkupSerializer による XOML へのシリアライズをおこない、その xoml ファイルを WorkflowCompiler でコンパイルします。(ご説明したように、これをメモリ中だけでおこなうことも可能です。)
ご説明したように、ワークフロー関連の一般的な dll 参照は WorkflowCompiler にデフォルトで組み込まれていますが、下記のように .NET Frameowrk 3.5 以降からの新しい dll などは参照を追加しておく必要がありますので、ここも要注意です。
private void completeBtn_Click(object sender, EventArgs e)
{
// 保存
loader.Flush();
// コンパイル実行
WorkflowCompiler compiler = new WorkflowCompiler();
WorkflowCompilerParameters parameters = new WorkflowCompilerParameters();
string[] compileFiles = new string[3];
compileFiles[0] = loader.FileName;
compileFiles[1] = Path.Combine(projectpath, "Workflow1.xoml.cs");
compileFiles[2] = Path.Combine(projectpath, "IWorkflow1.cs");
// (以下、ルールファイルがある場合のサンプル)
//string ruleFile = ... ;
//string resources = @"/resource:" + ruleFile + ",namespace.type.rules";
//parameters.CompilerOptions += resources;
parameters.ReferencedAssemblies.Add(Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles"), @"Reference Assemblies\Microsoft\Framework\v3.0\System.ServiceModel.dll"));
parameters.ReferencedAssemblies.Add(Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles"), @"Reference Assemblies\Microsoft\Framework\v3.5\System.WorkflowServices.dll"));
parameters.ReferencedAssemblies.Add(@"..\..\..\CustomActivityLibrary\bin\Debug\CustomActivityLibrary.dll");
parameters.OutputAssembly = Path.Combine(Directory.GetCurrentDirectory(), @"..\..\..\CustomWorkflowConsole\bin\Debug\CustomWorkflowLibrary.dll");
WorkflowCompilerResults results = compiler.Compile(parameters, compileFiles);
// Error 処理 (Warning は無視)
int i;
for (i = 0; i < results.Errors.Count; i++)
{
if (!results.Errors[i].IsWarning)
{
MessageBox.Show(results.Errors[i].ErrorText, "コンパイルの報告", MessageBoxButtons.OK, MessageBoxIcon.Error);
break;
}
}
if(i == results.Errors.Count)
MessageBox.Show("コンパイル完了です!");
else
MessageBox.Show("エラーが発生しました");
}