Uncategorized

[T2-401 デモ (4)] Custom の SharePoint Workflow Editor

環境 :
Visual Studio 2008 (.NET Framework 3.5)
SharePoint Server 2007

こんにちは。

  1. WCF の Transport レベルの Custom Channel Sample
  2. Rehosting を使ったエンドユーザへのワークフロー公開
  3. WF による Rule Base のアプリケーション
  4. Custom の SharePoint Workflow Editor

製品開発者向けということで実施した Tech Ed の T2-401 セッションですが、お約束通り、デモを添付しておきます。

ここまでの内容を元に、これを SharePoint に応用した、さいごにお見せしたサンプルです。(ライセンスの問題もお約束通り修正しておきました . . .)

download sample (20080826_SampleSPWorkflowCreator)

セッションの中で 1 つ言い忘れてしまったのですが、xoml の作成部分については、これまでの説明とまったく同様、普通にワークフローを構築して、いつものように WorkflowMarkupSerializer にお任せで OK です。(その部分のコードは、ソースをじっくりとみておいてください。) 違っている点とすれば、SharePoint 用のアクティビティを使っているという点だけです。

配置方法ですが、セッションでご説明したように、製品向けなどで SharePoint のワークフローを配置する際は、stsadm による 「ワークフローテンプレート」ではなく、 SharePoint Designer が使用している「ワークフロー」のデプロイのほうがさまざまな点で優れていました。
SharePoint では、WorkflowRuntime を直接起動して操作することは禁止されおり、ここでのポイントは SharePoint がワークフロー構築のために用意している (主に SharePoint Designer で使用されている)、ValidateWorkflowMarkupAndCreateSupportObjects、AssociateWorkflowMarkup などのメソッドが使えることでした。(http://msdn.microsoft.com/en-us/library/bb417436.aspx)
このため、SharePoint 展開用の「どのタスクリストを使うの ?」、「どのワークフローフォルダに入れるの ?」、「どのリストに関連付けるの ?」などの設定情報を記載したワークフロー構成ファイル (.xoml.wfconfig.xml) が必要で、この作成もおこなう必要があったわけです。(ただ、この定義は非常にシンプルなものですので、サンプルコードのように、XmlDocument などを使って自作すれば OK です)

あとは WorkflowMarkupSerializer などを使ってこれまでと同じように処理をおこなえば良いのですが、ご説明したように、SharePoint 固有の他のさまざまな課題もクリアしなければなりません。
例えば、ワークフローを関連付けるリストは、リストの表示名ではなく GUID が必要となります。また、列についても、表示名ではなく内部名が必要となります。また、ワークフローも、ただのドキュメントライブラリではなく、ご説明した ServerTemplate ID が 117 番のワークフロー用のフォルダに入れておく必要があります。つまり、このフォルダがない場合には、このフォルダを作るという作業も必要になるわけです。
(さらに、ここではサンプルを簡単にするためにワークフローフォームも使っていませんが、カスタムの初期化フォームや、タスクフォームが必要な場合には、こうしたフォームも aspx などで構築しておき、配置する必要もあります。)

コードでは、これら事前の処理を実施するため、リスト Web サービス (Lists.asmx) を使用した事前のやりとりをおこっています。またファイルの配置などは、WebRequest オブジェクトを使って WebDAV フォルダにアクセスしています。

さいごに、セッションの終わりでご説明した点ですが、時間が迫っていて簡単にしか説明できませんでしたので、ここはちゃんと記載しておきましょう。SharePoint Designer が作成した xoml ですが、セッションでお見せしたように、参照しているアクティビティのライブラリ名が、

Microsoft.SharePoint.WorkflowActions.dll, Version=12.0.0.0, Culture=neutral, PublicKeyToken=null

となっていました。
これは、SharePoint Designer が、リモートからも登録されているアクティビティの dll を扱ってワークフローの作成と配置ができるために必要な仕様で、Sharepoint Designer は %userprofile%\AppData\Roaming\Microsoft\SharePoint Designer\Proxy\Assembly\Cache\12.0.0.6219 の中に、サーバ上の dll と同じクラスやメソッドを持った dll の Proxy を内部で生成しています。このため、プロフェッショナル開発者が自作したカスタムの dll (SharePoint Designer のアクティビティ) なども、ちゃんと Proxy がキャッシュされ、リモートからも正しく動作するようになっています。(よって、公開キーまではコピーできませんので、PublicKeyToken を null にして配置する必要があります。)
では、この処理と同じことを皆さんの製品の中でも実施したくなるかもしれませんが、セッションでご紹介した FetchLegalWorkflowActions などの Web サービスのメソッドは、ご説明したように製品が内部で使用しているもので、一般の開発者に公開されたメソッドではありません。そこで上記のダウンロードサンプルでは、サーバ側の Microsoft.SharePoint.WorkflowActions.dll と同じクラスやメソッドを持つスタブ用のコードを作成し、これをワークフロー構築で使用するようにしています。(xoml 構築のためのクラスですので、内部処理は必要ありません。)
よって、WebPart の開発などでサーバ側で動作するモジュールの場合にはこうした面倒なスタブコードは必要ありませんし、リモートで処理されるワークフローであっても、初期化処理 (OnWorkflowActivated など) 以降はカスタムアクティビティだけで構成するようにしておけば、(初期化部分のみ xoml のテンプレートなどを用意しておき) Microsoft のライブラリを真似てスタブを構成しておく必要もありません。一般的には、こうした方法で開発するほうが望ましいでしょう。

以下にこのスタブコード以外の処理コードの部分を掲載しておきます。

public partial class Form1 :Form
{
    public Form1()
    {
        InitializeComponent();
    }
 
    ComboBox[] Conditions = new ComboBox[3];
    ComboBox[] Tasks = new ComboBox[3];
    TextBox[] MailAddresses = new TextBox[3];
    TextBox[] FieldNames = new TextBox[3];
    TextBox[] FieldValues = new TextBox[3];
    TextBox[] Approvers = new TextBox[3];
 
    XmlDocument xomlDoc;
    XmlDocument configDoc;
 
    private void Form1_Load(object sender, EventArgs e)
    {
        Tasks[0] = Task0;
        Tasks[1] = Task1;
        Tasks[2] = Task2;
 
        MailAddresses[0] = MailAddress0;
        MailAddresses[1] = MailAddress1;
        MailAddresses[2] = MailAddress2;
 
        FieldNames[0] = FieldName0;
        FieldNames[1] = FieldName1;
        FieldNames[2] = FieldName2;
 
        FieldValues[0] = FieldValue0;
        FieldValues[1] = FieldValue1;
        FieldValues[2] = FieldValue2;
    }
 
    private void ExecuteButton_Click(object sender, EventArgs e)
    {
        // Step 0 : Workflow Location などの設定
        Step_SetupObjects();
 
        // Step 1 : Workflow Config を作成
        Step_CreateWorkflowConfig();
 
        // Step 2 : XOML を作成
        Step_CreateXoml();
 
        // Step 3 : Upload !
        Step_UploadFiles();
 
        // Step 4 : ワークフローのコンパイル
        WebPartPagesSvc.WebPartPagesWebService sv = newWebPartPagesSvc.WebPartPagesWebService();
        sv.Url = string.Format("{0}/{1}", SiteLoc.Text.TrimEnd(new char[] { '/' }), "_vti_bin/WebPartPages.asmx");
        sv.UseDefaultCredentials = true;
        sv.Credentials = System.Net.CredentialCache.DefaultCredentials;
        sv.PreAuthenticate = true;
 
        sv.ValidateWorkflowMarkupAndCreateSupportObjects(xomlDoc.InnerXml, "", configDoc.InnerXml, "2");
        sv.AssociateWorkflowMarkup(string.Format("Workflows/{0}/{0}.xoml.wfconfig.xml", WorkflowName.Text), "V1.0");
 
        MessageBox.Show("ワークフローを配置しました");
    }
 
    private void Step_SetupObjects()
    {
        string taskListGUID = null;
        string workflowLibGUID = null;
 
        ListsSvc.Lists sv = new ListsSvc.Lists();
        sv.Url = string.Format("{0}/{1}", SiteLoc.Text.TrimEnd(new char[] { '/' }), "_vti_bin/Lists.asmx");
        sv.UseDefaultCredentials = true;
        sv.Credentials = System.Net.CredentialCache.DefaultCredentials;
        sv.PreAuthenticate = true;
 
        XmlNode listCol = sv.GetListCollection();
        foreach (XmlNode list in listCol)
        {
            // Get Task List ID
            if (list.Attributes["ServerTemplate"].Value == "107")
                taskListGUID = list.Attributes["ID"].Value;
            // Get No-Code Workflow Location ID
            else if (list.Attributes["ServerTemplate"].Value == "117")
                workflowLibGUID = list.Attributes["ID"].Value;
        }
 
        if (string.IsNullOrEmpty(taskListGUID))
            sv.AddList("Tasks", "Tasks", 107);
        if (string.IsNullOrEmpty(workflowLibGUID))
            sv.AddList("Workflows", "Workflows", 117);
    }
 
    private void Step_CreateWorkflowConfig()
    {
        string docListGUID = GetListGUIDFromName(SiteLoc.Text, AssocDocLib.Text);
        string taskListGUID, workflowLibGUID;
        GetWorkflowMetadata(SiteLoc.Text, out taskListGUID, out workflowLibGUID);
 
        // Root
        configDoc = new XmlDocument();
        XmlElement elemRoot = configDoc.CreateElement("WorkflowConfig");
        configDoc.AppendChild(elemRoot);
 
        // Template 要素
        XmlElement elemTemplate = configDoc.CreateElement("Template");
        XmlAttribute attBaseID = configDoc.CreateAttribute("BaseID");
        attBaseID.Value = "{" + Guid.NewGuid().ToString() + "}";
        elemTemplate.Attributes.Append(attBaseID);
        XmlAttribute attDocLibID = configDoc.CreateAttribute("DocLibID");
        attDocLibID.Value = workflowLibGUID;
        elemTemplate.Attributes.Append(attDocLibID);
        XmlAttribute attXomlHref = configDoc.CreateAttribute("XomlHref");
        attXomlHref.Value = string.Format("Workflows/{0}/{0}.xoml", WorkflowName.Text);
        elemTemplate.Attributes.Append(attXomlHref);
        XmlAttribute attXomlVersion = configDoc.CreateAttribute("XomlVersion");
        attXomlVersion.Value = "V1.0";
        elemTemplate.Attributes.Append(attXomlVersion);
        elemRoot.AppendChild(elemTemplate);
 
        // Association 要素
        XmlElement elemAssociation = configDoc.CreateElement("Association");
        XmlAttribute attListID = configDoc.CreateAttribute("ListID");
        attListID.Value = docListGUID;
        elemAssociation.Attributes.Append(attListID);
        XmlAttribute attTaskListID = configDoc.CreateAttribute("TaskListID");
        attTaskListID.Value = taskListGUID;
        elemAssociation.Attributes.Append(attTaskListID);
        XmlAttribute attStartOnCreate = configDoc.CreateAttribute("StartOnCreate");
        attStartOnCreate.Value = "true";
        elemAssociation.Attributes.Append(attStartOnCreate);
        elemRoot.AppendChild(elemAssociation);
 
        // ContentTypes 要素
        XmlElement elemContentTypes = configDoc.CreateElement("ContentTypes");
        elemRoot.AppendChild(elemContentTypes);
 
        // Initiation 要素
        XmlElement elemInitiation = configDoc.CreateElement("Initiation");
        XmlAttribute attURL = configDoc.CreateAttribute("URL");
        attURL.Value = "None";    // もしある場合は, .aspx を作成して相対パスを記載する
        elemInitiation.Attributes.Append(attURL);
        {
            // Fields 要素
            XmlElement elemFields = configDoc.CreateElement("Fields");
            elemInitiation.AppendChild(elemFields);
 
            // Parameters 要素
            XmlElement elemParameters = configDoc.CreateElement("Parameters");
            elemInitiation.AppendChild(elemParameters);
        }
        elemRoot.AppendChild(elemInitiation);
    }
 
    private void Step_CreateXoml()
    {
        // Root アクティビティ
        RootWorkflowActivityWithData rootActivity = new RootWorkflowActivityWithData();
        rootActivity.Name = "ROOT";
        rootActivity.WorkflowFields.Add(new WorkflowDataField("__list", "System.String"));
        rootActivity.WorkflowFields.Add(new WorkflowDataField("__item", "System.Int32"));
        rootActivity.WorkflowFields.Add(new WorkflowDataField("__context", "Microsoft.SharePoint.WorkflowActions.WorkflowContext"));
        rootActivity.WorkflowFields.Add(new WorkflowDataField("__initParams", "Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties"));
        rootActivity.WorkflowFields.Add(new WorkflowDataField("__workflowId", "System.Guid"));
 
        // OnWorkflowActivated アクティビティ
        OnWorkflowActivated onWorkflowActivatedActivity = new OnWorkflowActivated();
        CorrelationToken onWorkflowActivatedColToken = new CorrelationToken("refObject");
        onWorkflowActivatedColToken.OwnerActivityName = "ROOT";
        onWorkflowActivatedActivity.CorrelationToken = onWorkflowActivatedColToken;
        ActivityBind onWorkflowActivatedBind = new ActivityBind();
        onWorkflowActivatedBind.Name = "ROOT";
        onWorkflowActivatedBind.Path = "__initParams";
        onWorkflowActivatedActivity.SetBinding(OnWorkflowActivated.WorkflowPropertiesProperty, onWorkflowActivatedBind);
        rootActivity.Activities.Add(onWorkflowActivatedActivity);
 
        // ApplyActivation アクティビティ
        ApplyActivation applyActivationActivity = new ApplyActivation();
        ActivityBind applyActivationBindContextProp = new ActivityBind();
        applyActivationBindContextProp.Name = "ROOT";
        applyActivationBindContextProp.Path = "__context";
        applyActivationActivity.SetBinding(ApplyActivation.__ContextProperty, applyActivationBindContextProp);
        ActivityBind applyActivationBindWorkflowProp = new ActivityBind();
        applyActivationBindWorkflowProp.Name = "ROOT";
        applyActivationBindWorkflowProp.Path = "__initParams";
        applyActivationActivity.SetBinding(ApplyActivation.__WorkflowPropertiesProperty, applyActivationBindWorkflowProp);
        rootActivity.Activities.Add(applyActivationActivity);
 
        // Sequence アクティビティ (ステップ 1)
        SequenceActivity step1Activity = new SequenceActivity();
        step1Activity.Description = "ステップ 1";
        for (int i = 0; i < 3; i++)
        {
            // SetField アクティビティ
            if (Tasks[i].SelectedItem == "Field Update")
            {
                SetFieldActivity setFieldActivity = new SetFieldActivity();
                string docListGUID = GetListGUIDFromName(SiteLoc.Text, AssocDocLib.Text);
                setFieldActivity.FieldName = GetFieldInternalName(SiteLoc.Text, docListGUID, FieldNames[i].Text);
                setFieldActivity.Value = FieldValues[i].Text;
 
                ActivityBind contextPropertyBind = new ActivityBind();
                contextPropertyBind.Name = "ROOT";
                contextPropertyBind.Path = "__context";
                setFieldActivity.SetBinding(SetFieldActivity.__ContextProperty, contextPropertyBind);
 
                ActivityBind listIdBind = new ActivityBind();
                listIdBind.Name = "ROOT";
                listIdBind.Path = "__list";
                setFieldActivity.SetBinding(SetFieldActivity.__ListIdProperty, listIdBind);
 
                ActivityBind listItemBind = new ActivityBind();
                listItemBind.Name = "ROOT";
                listItemBind.Path = "__item";
                setFieldActivity.SetBinding(SetFieldActivity.__ListItemProperty, listItemBind);
 
                step1Activity.Activities.Add(setFieldActivity);
            }
            // EMail アクティビティ
            else if (Tasks[i].SelectedItem == "Send Mail")
            {
                EmailActivity eMailActivity = new EmailActivity();
 
                ArrayList toArray = new ArrayList();
                toArray.Add(MailAddresses[i].Text);
                eMailActivity.To = toArray;
                eMailActivity.CC = null;
                eMailActivity.BCC = null;
 
                eMailActivity.Subject = "Custom Workflow Test";
                eMailActivity.Body = "Hello, SharePoint !";
 
                ActivityBind contextPropertyBind = new ActivityBind();
                contextPropertyBind.Name = "ROOT";
                contextPropertyBind.Path = "__context";
                eMailActivity.SetBinding(EmailActivity.__ContextProperty, contextPropertyBind);
 
                step1Activity.Activities.Add(eMailActivity);
            }
        }
        rootActivity.Activities.Add(step1Activity);
 
        // XmlDocument に保存
        MemoryStream xomlMem = new MemoryStream();
        XmlTextWriter xomlWriter = new XmlTextWriter(xomlMem, Encoding.UTF8);
        WorkflowMarkupSerializer xomlSerializer = new WorkflowMarkupSerializer();
        xomlSerializer.Serialize(xomlWriter, rootActivity);
        xomlDoc = new XmlDocument();
        xomlMem.Position = 3; // Attention! : 先頭の BOM (Byte Order Mark) は SharePoint でおかしな動きになる !
        xomlDoc.Load(xomlMem);
        xomlWriter.Close();
        xomlMem.Close();
 
        // コードも含めたコンパイル (サーバ側) に備え x:Class を追加
        XmlAttribute classAttr = xomlDoc.CreateAttribute("Class", @"http://schemas.microsoft.com/winfx/2006/xaml");
        classAttr.Value = "Microsoft.SharePoint.Workflow.ROOT";
        xomlDoc.ChildNodes[0].Attributes.Append(classAttr);
 
        // if you use Microsoft.SharePoint.WorkflowActions.dll,
        // this code is needed.
        // xomlDoc.ChildNodes[0].Attributes["xmlns:ns0"].Value = @"clr-namespace:Microsoft.SharePoint.WorkflowActions;Assembly=Microsoft.SharePoint.WorkflowActions, Version=12.0.0.0, Culture=neutral, PublicKeyToken=null";
 
 
    }
 
    private void Step_UploadFiles()
    {
        byte[] buf = new byte[1024];
 
        // Workflow Folder の作成
        string folderUrl = string.Format("{0}/Workflows/{1}", SiteLoc.Text.TrimEnd(new char[] { '/' }), WorkflowName.Text);
        WebRequest folderRequest = WebRequest.Create(folderUrl);
        folderRequest.UseDefaultCredentials = true;
        folderRequest.Credentials = System.Net.CredentialCache.DefaultCredentials;
        folderRequest.PreAuthenticate = true;
        folderRequest.Method = "MKCOL";
        folderRequest.GetResponse().Close();
 
        // Workflow Config のアップロード
        string configUrl = string.Format("{0}/Workflows/{1}/{1}.xoml.wfconfig.xml", SiteLoc.Text.TrimEnd(new char[] { '/' }), WorkflowName.Text);
        WebRequest configRequest = WebRequest.Create(configUrl);
        configRequest.UseDefaultCredentials = true;
        configRequest.Credentials = System.Net.CredentialCache.DefaultCredentials;
        configRequest.PreAuthenticate = true;
        configRequest.Method = "PUT";
 
        MemoryStream configMem = new MemoryStream();
        configDoc.Save(configMem);
        using (Stream reqStream = configRequest.GetRequestStream())
        {
            configMem.Seek(0, SeekOrigin.Begin);
            for (int byteCount = configMem.Read(buf, 0, buf.Length); byteCount > 0; byteCount = configMem.Read(buf, 0, buf.Length))
            {
                reqStream.Write(buf, 0, byteCount);
            }
        }
        configMem.Close();
        configRequest.GetResponse().Close();
 
        // Xoml のアップロード
        string xomlUrl = string.Format("{0}/Workflows/{1}/{1}.xoml", SiteLoc.Text.TrimEnd(newchar[] { '/' }), WorkflowName.Text);
        WebRequest xomlRequest = WebRequest.Create(xomlUrl);
        xomlRequest.UseDefaultCredentials = true;
        xomlRequest.Credentials = System.Net.CredentialCache.DefaultCredentials;
        xomlRequest.PreAuthenticate = true;
        xomlRequest.Method = "PUT";
 
        MemoryStream xomlMem = new MemoryStream();
        xomlDoc.Save(xomlMem);
        using (Stream reqStream = xomlRequest.GetRequestStream())
        {
            xomlMem.Seek(0, SeekOrigin.Begin);
            for (int byteCount = xomlMem.Read(buf, 0, buf.Length); byteCount > 0; byteCount = xomlMem.Read(buf, 0, buf.Length))
            {
                reqStream.Write(buf, 0, byteCount);
            }
        }
        xomlMem.Close();
        xomlRequest.GetResponse().Close();
    }
 
    #region Helper Functions
 
    // フィールド名称からフィールドの内部名を取得する
    private string GetFieldInternalName(string pSiteLocation, string pDocListGUID, stringpDisplayName)
    {
        string fieldName = null;
 
        ListsSvc.Lists sv = new ListsSvc.Lists();
        sv.Url = string.Format("{0}/{1}", pSiteLocation.TrimEnd(new char[] { '/' }), "_vti_bin/Lists.asmx");
        sv.UseDefaultCredentials = true;
        sv.Credentials = System.Net.CredentialCache.DefaultCredentials;
        sv.PreAuthenticate = true;
        XmlNode listNode = sv.GetList(pDocListGUID);
        XmlElement fieldsElem = listNode["Fields"];
        foreach (XmlElement field in fieldsElem)
        {
            if (field.Attributes["DisplayName"].Value == pDisplayName)
                fieldName = field.Attributes["Name"].Value;
        }
        return fieldName;
    }
 
    // デフォルトのタスクリストIDなど、
    // ワークフロー作成に必要なデータを収集する
    private void GetWorkflowMetadata(string pSiteLocation, out string pTaskListGUID, out stringpWorkflowLibGUID)
    {
        pTaskListGUID = null;
        pWorkflowLibGUID = null;
 
        ListsSvc.Lists sv = new ListsSvc.Lists();
        sv.Url = string.Format("{0}/{1}", pSiteLocation.TrimEnd(new char[] { '/' }), "_vti_bin/Lists.asmx");
        sv.UseDefaultCredentials = true;
        sv.Credentials = System.Net.CredentialCache.DefaultCredentials;
        sv.PreAuthenticate = true;
        XmlNode listCol = sv.GetListCollection();
        foreach (XmlNode list in listCol)
        {
            // Get Task List ID
            if(list.Attributes["ServerTemplate"].Value == "107")
                pTaskListGUID = list.Attributes["ID"].Value;
            // Get No-Code Workflow Location ID
            else if (list.Attributes["ServerTemplate"].Value == "117")
                pWorkflowLibGUID = list.Attributes["ID"].Value;
        }
    }
 
    // List Web サービスから List の Guid を取得する
    private string GetListGUIDFromName(string siteLocation, string pListName)
    {
        ListsSvc.Lists sv = new ListsSvc.Lists();
        sv.Url = string.Format("{0}/{1}", siteLocation.TrimEnd(new char[] { '/' }), "_vti_bin/Lists.asmx");
        sv.UseDefaultCredentials = true;
        sv.Credentials = System.Net.CredentialCache.DefaultCredentials;
        sv.PreAuthenticate = true;
        XmlNode list = sv.GetListAndView(pListName, "");
        return list.ChildNodes[0].Attributes["Name"].Value;
    }
 
    #endregion Helper Functions
 
}
Advertisements

Categories: Uncategorized

Tagged as:

8 replies »

  1. hi thanks for the post it is great
    i have downloaded the sample and run it
    it works fine but when the code reaches WebPartPagesWebService.AssociateWorkflowMarkup
    it gives this error
    Exception of type ‘Microsoft.SharePoint.SoapServer.SoapServerException’ was thrown.
    and i search for more details inside the error and i get that the StackTrace is
      at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
      at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
      at WorkflowWebApplication.sharepointdev1.WebPartPagesWebService.AssociateWorkflowMarkup(String configUrl, String configVersion)
    so could anyone help me to know why this error is happening

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s