概要
本日紹介するブログ投稿ゲストは、SharePoint の開発および統合に重点的に取り組んでいる、ジョージア州アルファレッタ市に本拠を置く Microsoft 認定ゴールド パートナーの ThreeWill 社です。ThreeWill は、最近、Microsoft と連携して、InfoPath Forms Services を使用した汎用のサービス要求 Office ビジネス アプリケーション (OBA) を構築しました。このアプリケーションは、コードを使用せずに、サービス要求に基づく SharePoint サイト (つまり、電子フォームを使用して要求を開始し、その要求をワークフローに結び付けるサイト) を迅速に立ち上げるという企業の要求に応えるものです。
サービス要求の例には、次のものがあります。
- マーケティング資金に対する要求
- ラップトップやその他の装置に対する要求
- プロジェクト サイトの作成に対する要求
このソリューションは、サーバー ファームへの展開と標準的な SharePoint プロビジョニングを可能にするための SharePoint 機能としてパッケージ化されています。このアプリケーションでは、Active Directory との統合がサポートされており、ユーザー情報が事前に設定され、データ接続を使用して InfoPath から Web サービスに簡単に接続できます。最終的には、構成情報はサイト管理者が安全、便利にアクセスできるように、SharePoint リストに格納されます。ThreeWill でこれをどのようにして実現したかについて見ていきましょう。
Pej Javaheri (SharePoint プロダクト マネージャー)
図 1 にこのサイトのホーム ページを示します。クイック起動ナビゲーションを見るとわかるとおり、ユーザーは要求を作成、編集、および表示できます。これによって、多くの目的に合わせて簡単に構成できるセルフ サービス アプリケーションを実現できます。
図 1 - サービス要求アプリケーションのホームページ
コアのユーザビリティ機能
このプロジェクトの主要な目的の 1 つは、ユーザーの操作性を向上するために、標準の InfoPath フォームの動作にいくつかの重要な変更を行うことでした。たとえば、クイック起動ナビゲーションの [Create Request] (要求の作成) リンクをクリックして要求の作成を開始すると、Web パーツ ページとして組み込まれた InfoPath フォームを使用した [Create/Edit Request] (要求の作成/編集) ページに移動します。ユーザーが要求を保存すると、その要求が自動生成された名前で保存され、ユーザーはサービス要求サイトのホーム ページに戻ります。こうしたフォームとのやり取りの "調整" によって、サイトの全体的なユーザー エクスペリエンスが向上します。
これらの機能を実装するために、このチームでは、XML フォーム ビューアーの 1 つのインスタンスを 1 つの Web パーツにラップしました。
標準の名前付け規則を適用するために、SubmitToHost イベントを上書きして、ユーザー名と現在の時刻に基づいた名前付け規則を使用してフォームを保存するようにしました。
void _xmlFormViewControl_SubmitToHost(object sender, SubmitToHostEventArgs e)
{
const string formLibraryName = "Requests";
SPWeb currentWeb = SPContext.Current.Web;
// Get the contents of the form and put them into a byte array
XPathNavigator nav = _xmlFormViewControl.XmlForm.MainDataSource.CreateNavigator();
System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
byte[] contents = enc.GetBytes(nav.OuterXml);
//load in the xml document so we can extract out fields by name
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.LoadXml(nav.OuterXml);
Dictionary<string, ArrayList> documentFields = Utility.GetNameValuePairs(xmlDocument);
//retrieve InfoPath form keys from the Configuration list
Dictionary<string, string> configuration = Utility.GetConfiguration(SPContext.Current.Web);
string [] formKeyFields = string.IsNullOrEmpty (configuration[Utility.ConstInfoPathFormKeysParameterColumn])?
new string [0]:
configuration[Utility.ConstInfoPathFormKeysParameterColumn].Split(',');
//begin to build the filename
string filename = "";
//iterate through each form key field
foreach (string formKeyField in formKeyFields)
{
//if the form key field is found in the InfoPath form, add it to the filename
if (documentFields.ContainsKey(formKeyField.Trim().ToUpper()))
{
foreach (string fieldValue in documentFields[formKeyField.Trim().ToUpper()])
{
filename += fieldValue;
filename += "_";
}
}
}
//name files using the user name and date if the infopath form keys could not be found
if (string.IsNullOrEmpty(filename))
{
filename = SPEncode.UrlEncode(currentWeb.CurrentUser.Name.Replace('\\', '-')) + "_" + DateTime.Now.ToString("yyyyMMdd_hhmmss") + ".xml";
}
else
{
//trim any training underscores, add the file extension
filename = SPEncode.UrlEncode(filename.Trim(new char[] { '_' })) + ".xml";
}
// Determine the URL of the new file
string saveUrl = currentWeb.Url + "/" + formLibraryName + "/" + filename;
// open document library as a folder
SPFolder docLibFolder = currentWeb.GetFolder(formLibraryName);
// Save the form by adding its contents to the document library
if (string.IsNullOrEmpty (_xmlLocation) == false && docLibFolder.Files[_xmlLocation] != null)
docLibFolder.Files[_xmlLocation].SaveBinary(contents);
else
docLibFolder.Files.Add(saveUrl, contents);
}
The Close event is where we handled sending the user back to the home page.
void _xmlFormViewControl_Close(object sender, EventArgs e)
{
// Inspect the QueryString
if (this.Page.Request.QueryString["Source"] != null)
{
SPUtility.Redirect("", SPRedirectFlags.UseSource, this.Context);
}
else
{
SPUtility.Redirect(SPContext.Current.Web.Url + "/default.aspx", SPRedirectFlags.CheckUrl, this.Context);
}
}
さらに、[Edit My Request] (要求の編集) リンクについても、ユーザービリティの向上を図りました。ユーザーがこのリンクをクリックすると、そのサイトに対する要求が 1 つのみの場合、その要求が読み込まれます。要求が 1 つもない場合は、新しい要求が作成されるようになります。また、複数の要求がある場合は、要求の一覧が表示されます。これらの場合の処理のために、ServiceRequestRedirector (サービス要求リダイレクター) クラスを作成しました。
public class ServiceRequestRedirector : LayoutsPageBase
{
private const string FORMS_LIST_NAME = "Requests";
private const string QUERY_DEF = "<Where><Or><Eq><FieldRef Name='Author'/><Value Type='User'>{0}</Value></Eq><Eq><FieldRef Name='Editor'/><Value Type='User'>{0}</Value></Eq></Or></Where>";
protected override void OnLoad(EventArgs e)
{
// get current site, web and user
SPSite siteCollection = this.Site;
SPWeb site = this.Web;
SPUser currentUser = site.CurrentUser;
//get list of fields to return "<Where><Or><Eq><FieldRef Name='Author'/><Value Type='User'>{0}</Value></Eq><Eq><FieldRef Name='Editor'/><Value Type='User'>{0}</Value></Eq></Or></Where>"
string viewFields = GetViewFields();
//build the query string for querying the Request list
//to retrieve any item created or modified by the current user
string requestQueryFields = string.Format(QUERY_DEF, currentUser.Name);
string redirectUrl = default(string);
try
{
SPListCollection lists = site.Lists;
SPList timesheetList = site.Lists[FORMS_LIST_NAME];
SPQuery requestQuery = new SPQuery();
requestQuery.ViewFields = viewFields;
requestQuery.Query = requestQueryFields;
SPListItemCollection listItems = timesheetList.GetItems(requestQuery);
if (listItems.Count == 0)
{
redirectUrl = site.Url + "/SitePages/norequests.aspx";
}
if (listItems.Count == 1)
{
string url = Request.RawUrl;
redirectUrl = site.Url + "/SitePages/editrequest.aspx?XmlLocation=" + site.Url + "/Requests/" + SPEncode.UrlEncode(listItems[0].Title) + "&Source=" + SPEncode.UrlEncode(site.Url + "/default.aspx");
}
if (listItems.Count > 1)
{
try
{
if (Request.QueryString["Title"].ToString() != "")
{
redirectUrl = site.Url + "/SitePages/editrequest.aspx?XmlLocation=" + site.Url + "/Requests/" + Request.QueryString["Title"] + "&Source=" + SPEncode.UrlEncode(site.Url + "/default.aspx");
}
}
catch (System.ArgumentOutOfRangeException)
{
//no Title in QueryString, just continue to display all items
redirectUrl = site.Url + "/Requests/Forms/MyItems.aspx";
}
catch (Exception ex)
{
//no Title in QueryString, just continue to display all items
redirectUrl = site.Url + "/Requests/Forms/MyItems.aspx";
}
}
}
catch (SPException spe)
{
System.Diagnostics.Debug.Print(spe.Message);
}
//redirect to the correct page for the number of requests
SPUtility.Redirect(redirectUrl, SPRedirectFlags.CheckUrl, HttpContext.Current);
}
実装されたその他の重要な機能
このソリューションには、InfoPath のデータ接続や SharePoint Designer のユーザー設定のアクションで使用される、再利用可能な Web サービスのプロビジョニングも含まれています。このため、サイト デザイナーは、InfoPath を使用して洗練されたフォームを設計する方法を知っておく必要があります。
Web サービスの機能には、次のものがあります。
- ログインしたユーザーに基づいて、ユーザー プロファイル情報をフォームに事前に設定するために AD に接続する。
- SharePoint グループを取得する (利用できる場合) - ブラウザー対応のフォームで "ロール ベース" の機能をシミュレートするために使用されます。
- リスト ベースのデータのフィルタリングを連鎖処理する - 親子関係で関連付けられているリスト データをユーザーが使用できるようにします (たとえば、"State" (州) のドロップ ダウンでは、"City" (市) のドロップ ダウンのフィルタリングが自動的に行われます)。
SharePoint のユーザー設定アクションに、SharePoint Designer の新しいユーザー設定アクション (実行時の承認者) が追加されました。これにより、実行時に承認者の一覧が提示される SharePoint 承認ワークフローが提供されます。
フォームは Active Directory からの情報を使用して事前に設定されています。
ユーザーはドロップ ダウン リストで項目を選択でき、他のフィールドはその選択に基づいて更新されます。
フォームで親子関係を表示できます。
図 2 - サービス要求の作成/編集例
これらのサービスを簡素化する際に、いくつか重要な教訓を得ました。Kirk は、匿名アクセスで Web サービスにアクセスして現在のユーザーと連携した動作を行おうとしたときに発見がありました。プロジェクトにおいて Windows SharePoint Services 3.0 でホストされた Web サービスの登録についても多くの教訓があり、Pete はこの問題への対処について、引き続きブログに投稿しています。また、一部の SharePoint Web サービスの呼び出しでは、入力/出力パラメーターに対して適切に定義されたスキーマが存在せず、また、入力/出力スキーマが InfoPath などのツールで簡単には実行できないため、簡素化した Web サービスのクラスを作成しました。
private SPList GetList(SPWeb site, string listName)
{
// Get the list
SPList list = null;
try
{
list = site.Lists[listName];
}
catch (Exception ex)
{
throw new Exception(String.Format("List [{0}] does not exist within site [{1}].", listName, site.Url), ex);
}
return list;
}
private List<ListItem> ExecuteQuery(SPList list, SPQuery spQuery, string fields, bool unique)
{
// Run the query
SPListItemCollection items = null;
try
{
items = list.GetItems(spQuery);
}
catch (Exception ex)
{
throw new Exception("Unable to execute query.", ex);
}
// Turn the output into our format
List<ListItem> response = new List<ListItem>();
Dictionary<String, String> uniqueItems = new Dictionary<String, String>();
foreach (SPListItem item in items)
{
StringBuilder uniqueSb = new StringBuilder();
// Populate a response item
ListItem responseItem = new ListItem();
responseItem.ID = item.ID;
responseItem.Title = item.Title;
responseItem.Fields = new List<Field>();
// Populate the fields for the response
foreach (string field in fields.Split(','))
{
string fieldName = field.Trim();
if (!item.Fields.ContainsField(fieldName))
{
throw new Exception(String.Format("Field [{0}] does not exist within list [{1}].", fieldName, list.Title));
}
Field responseField = new Field();
responseField.Name = fieldName;
object fieldValue = item[fieldName];
responseField.Value = (fieldValue != null ? fieldValue.ToString() : String.Empty);
responseItem.Fields.Add(responseField);
// Keep track of a unique key if we care about uniqueness
if (unique)
{
uniqueSb.Append(responseField.Value).Append('\0');
}
}
// Include the list item in the responses
// But don't include it if the user requested unique values and this item is not unique
if (!unique)
{
response.Add(responseItem);
}
else
{
string uniqueKey = uniqueSb.ToString();
if (!uniqueItems.ContainsKey(uniqueKey))
{
response.Add(responseItem);
uniqueItems.Add(uniqueKey, null);
}
}
}
return response;
}
リスト ベースの構成
サイトの管理を簡略化するために、構成はサイト管理者のみがアクセスできる SharePoint リスト内に格納されます。構成パラメーターは名前と値のペアとして格納され、サイト管理者は使い慣れた SharePoint インターフェイスを通して構成データに容易にアクセスできます。
図 3 - サービス要求アプリケーションの構成
まとめ
このソリューションの利点は、コードの作成が不要で、多くの目的に合わせて構成できるエンタープライズ クラスのサービス要求アプリケーションであることです。このアプリケーションには、フォーム名の自動生成、フォームとの連携動作のプロセスの簡素化など、多くの重要なユーザビリティにおける強化点があります。InfoPath を使用して簡単にアプリケーション用のフォームを構築でき、Active Directory からの情報の事前設定、SharePoint リストのデータに基づいたフォームの構築など、使用できる多くの重要なサービスがあります。アプリケーションの構成情報は、SharePoint リストに格納され、簡単、安全にアクセスできます。
このプロジェクトのチーム メンバー
このプロジェクトに参加した主なチーム メンバーは、Pete Skelly、Eric Bowden、Kirk Liemohn、Chris Edwards、Jerry Rasmussen です。このプロジェクトの概念を実現させるために熱心に働きかけていただいた、Microsoft の Donna Hodges 氏に謝意を表します。最後に、業務上の連絡窓口として、ビジネス要件の特定を支援し、業務上の問題を効果的に解決するために必要なリソースをクライアントおよび Microsoft から引き出すことに尽力していただいた、Microsoft の Kern Sutton 氏に謝意を表します。
執筆者について
Pete Skelly - Pete は ThreeWill のシニア コンサルタントです。Microsoft .NET を使用した MCSD、および SharePoint Services 3.0 アプリケーション開発の MCTS として認定を受けていると共に、Certified ScrumMaster (認定スクラムマスター) でもあります。Pete はゴルフを趣味以上のものにしたいと考えていますが、2 人の息子と遊ぶこととプレイ代のために思いどおりにいきません。
Eric Bowden - Eric は ThreeWill のシニア コンサルタントです。趣味のランニング、キャンプ、家族との野外活動、さらに、理想的な通勤時間を楽しんでいます。'Eric は MCSD の認定も受けていますが、それに止まらず、実際には、たいへんな SharePoint 開発者のスキルを持ったプログラマです。
Danny Ryan - Danny は ThreeWill の共同創業者の 1 人で、顧客と SharePoint によるソリューションの構築に取り組んでいるとき以外は、家で 2 人の小さな娘と遊ぶことを楽しみにしています。Danny は、"SharePoint for the Enterprise" という月刊のニュースレターを執筆しています。このニュースレターでは、"エンタープライズ クラス" の SharePoint アプリケーションの効果的な構築に関するトピックを取り上げています (http://www.threewill.com/newsletter/)。
これは、ローカライズされたブログ投稿です。原文の記事は、http://blogs.msdn.com/sharepoint/archive/2008/11/14/how-we-did-it-automating-service-requests-using-infopath-forms-services.aspx をご覧ください。