SharePoint 项目的 Azure 自定义声明提供程序(第 3 部分)
原文发布于 2012 年 2 月 21 日(星期二)
在本系列的第 1 部分中,我简要地阐述了此项目的目标,简单地说就是,使用 Windows Azure 表存储作为 SharePoint 自定义声明提供程序的数据存储。该声明提供程序将使用 CASI 工具包(该链接可能指向英文页面)从 Windows Azure 检索所需的数据,以便提供人员选取器(即通讯簿)和键入控件名称解析功能。
在第 2 部分中,我介绍了云中运行的所有组件 - 用于处理 Azure 表存储和队列的数据类、从队列读取项目并填充表存储的辅助角色,以及允许客户端应用程序在队列中创建新项目并执行所有标准的 SharePoint 人员选取器操作的 WCF 前端,例如提供受支持声明类型的列表、搜索声明值和解析声明。
在本系列的这最后一部分中,我们将讨论 SharePoint 端使用的不同组件。包括使用 CASI 工具包构建的自定义组件,以便向队列添加项目以及对 Azure 表存储执行调用。另外还包括我们的自定义声明提供程序,它使用 CASI 工具包组件连接 SharePoint 与那些 Azure 功能。
让我们先快速地了解下自定义 CASI 工具包组件。我不打算详细对其进行介绍,因为该博客网站上到处都有关于 CASI 工具包的讨论。此特定的组件在 CASI 工具包系列的第 3 部分有介绍。简而言之,我所做的就是创建了一个新的 Windows 类库项目。我添加了对 CASI 工具包基类程序集和其他必需的 .NET 程序集(在第 3 部分中介绍)的引用。我在项目中添加了对在此项目第 2 部分中创建的 WCF 终结点的服务引用。最后,我向项目添加了一个新类,让其继承 CASI 工具包基类,并添加代码以重写 ExecuteRequest 方法。就像您可能已在 CASI 工具包系列中看到的那样,我重写 ExecuteRequest 的代码如下所示:
public class DataSource : AzureConnect.WcfConfig
{
public override bool ExecuteRequest()
{
try
{
//create the proxy instance with bindings and endpoint the base class
//configuration control has created for this
AzureClaims.AzureClaimsClient cust =
new AzureClaims.AzureClaimsClient(this.FedBinding,
this.WcfEndpointAddress);
//configure the channel so we can call it with
//FederatedClientCredentials.
SPChannelFactoryOperations.ConfigureCredentials<AzureClaims.IAzureClaims>
(cust.ChannelFactory,
Microsoft.SharePoint.SPServiceAuthenticationMode.Claims);
//create a channel to the WCF endpoint using the
//token and claims of the current user
AzureClaims.IAzureClaims claimsWCF =
SPChannelFactoryOperations.CreateChannelActingAsLoggedOnUser
<AzureClaims.IAzureClaims>(cust.ChannelFactory,
this.WcfEndpointAddress,
new Uri(this.WcfEndpointAddress.Uri.AbsoluteUri));
//set the client property for the base class
this.WcfClientProxy = claimsWCF;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
//now that the configuration is complete, call the method
return base.ExecuteRequest();
}
}
“AzureClaims”是我创建的服务引用的名称,它使用我在 Azure 的 WCF 项目中定义的 IAzureClaims 接口。正如之前在 CASI 工具包系列中所述的一样,这基本上是样板文件代码,我刚刚插入了将在 WCF 应用程序中显示的接口和类的名称。我做的另外一件事情就是创建名为 AzureClaimProvider.aspx 的 ASPX 页,这在 CASI 工具包系列中也有介绍。我刚刚复制和粘贴了我在 CASI 工具包系列的第 3 部分介绍的代码,并替代了我的类以及它可以到达的终结点的名称。我自定义 CASI 工具包组件的 ASPX 页的控件标记如下所示:
<AzWcf:DataSource runat="server" id="wcf" WcfUrl="https://spsazure.vbtoys.com/ AzureClaims.svc" OutputType="Page" MethodName="GetClaimTypes" AccessDeniedMessage="" />
这里值得注意的是,我为指向我在 cloudapp.net 中的 Azure 应用程序的“spsazure.vbtoys.com”创建了 CNAME 记录(这在 CASI 工具包的第 3 部分同样也有介绍)。我已经将页面要调用的默认 MethodName 设置为 GetClaimTypes,它是一个不使用参数的方法,并返回我的 Azure 声明提供程序支持的声明类型列表。这是一项很好的测试,可验证我的 Azure 应用程序和 SharePoint 之间的连接。我只需转至 https://anySharePointsite/_layouts/AzureClaimProvider.aspx,如果一切都配置正确,我将在页面中看到一些数据。我通过将程序集添加到全局程序集缓存并将页面部署到 SharePoint 的 _layouts 目录中(我所做的就是这些)来部署了我的项目后,在我的一个网站中点击该页面并验证是否返回数据,这样我便知道我在 SharePoint 和 Azure 之间的连接是否有效。
一切准备就绪之后,我终于到达了项目“有趣”的部分,有两件事情要做:
- 创建“某个组件”以便将有关新用户的信息发送到我的 Azure 队列
- 创建自定义声明提供程序以使用我的自定义 CASI 工具包组件提供声明类型、名称解析和搜索声明。
在这里,我们可以先暂停并简单地回顾一下。在此特定情形中,我只是想尽可能快地显示数据。所以,我所做的就是创建一个新的 Web 应用程序并启用匿名访问。我相信你们都知道,只是在 Web 应用程序级别启用匿名访问不会导致在网站集级别启用匿名访问。所以对于此应用场景,我也是仅在根网站集启用了匿名访问;我授予了对网站上所有内容的访问权限。所有其他仅包含成员信息的网站集未启用匿名访问,所以必须授予用户权限才能加入。
下一件要考虑的事情是如何管理将使用网站的身份。很明显,这可不是我想做的事情。我可以想出有许多不同的方法将帐户同步到 Azure 或类似事物中,但是就像我在本系列的第 1 部分中所说的那样,已经有一大堆的提供商在做这件事情,所以我打算让他们继续做他们所做的。我的意思是,我打算使用另一个 Microsoft 云服务(名为 ACS,即访问控制服务)。总之,ACS 将充当 SharePoint 的身份提供程序。所以,我只是在 SharePoint 场和我为此 POC 创建的 ACS 实例之间建立了信任。在 ACS 中,我将 SharePoint 添加为信赖方,以便 ACS 知道当用户通过身份验证后将其发送至何处。在 ACS 内,我还将其配置为允许用户使用他们的 Gmail、Yahoo 或 Facebook 帐户进行登录。他们登录后,ACS 返回一个我要使用的声明(即电子邮件地址),并将其发送回 SharePoint。
好了,这就是全部的背景知识。Azure 提供表存储和队列以处理数据,ACS 提供身份验证服务,而 CASI 工具包提供数据的管道。
上述一切准备就绪之后,我们要如何使用它呢?我仍然想让此过程更方便一些,所以我编写了一个 Web 部件来将用户添加到我的 Azure 队列。它所做的就是检查以确定请求是否通过了身份验证(即用户单击了您匿名网站上的登录链接,并登录到上述的一个提供程序,ACS 将其声明信息发送给我)。如果请求未通过身份验证,该 Web 部件不执行任何操作。但是,如果请求通过了身份验证,该 Web 部件将显示一个按钮,单击该按钮可将用户的电子邮件声明添加到 Azure 队列中。这就是我所说的暂停回顾一下的地方,有些事情需要我们思考。对于 POC 来说,一切都没问题。但是,您可以想想其他方法处理该请求。例如,您可以将信息写入 SharePoint 列表。您可以编写一个自定义计时器作业(CASI 工具包可以很好地处理)并定期处理该列表中的新请求。您可以使用 SPWorkItem 对请求排队以便稍后处理。您可以将其存储在列表中并添加一个可能要处理某个审批流程的自定义工作流,请求通过审批后,使用自定义工作流操作调用 CASI 工具包以便将详细信息写入 Azure 队列。总之,可在这里实现许多功能、很强的灵活性和大量自定义操作,只需发挥您的想象力即可。实际上在有些时候我可以另外编写一种版本将其写入自定义列表,在某个时候执行异步处理,将数据添加到 Azure 队列,然后自动将帐户添加到某个子网站的 Visitors 组中,这样用户将被注册并可随意访问。但是即使我要这么做,那也是在另一篇博客中。
所以,如上所述我所做的只是允许用户登录后单击一个按钮,然后使用我的自定义 CASI 工具包组件显示 WCF 终结点并将信息添加到 Azure 队列。下面是 Web 部件的代码,多亏有了 CASI 工具包,代码才会如此简单:
public class AddToAzureWP : WebPart
{
//button whose click event we need to track so that we can
//add the user to Azure
Button addBtn = null;
Label statusLbl = null;
protected override void CreateChildControls()
{
if (this.Page.Request.IsAuthenticated)
{
addBtn = new Button();
addBtn.Text = "Request Membership";
addBtn.Click += new EventHandler(addBtn_Click);
this.Controls.Add(addBtn);
statusLbl = new Label();
this.Controls.Add(statusLbl);
}
}
void addBtn_Click(object sender, EventArgs e)
{
try
{
//look for the claims identity
IClaimsPrincipal cp = Page.User as IClaimsPrincipal;
if (cp != null)
{
//get the claims identity so we can enum claims
IClaimsIdentity ci = (IClaimsIdentity)cp.Identity;
//look for the email claim
//see if there are claims present before running through this
if (ci.Claims.Count > 0)
{
//look for the email address claim
var eClaim = from Claim c in ci.Claims
where c.ClaimType == "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
select c;
Claim ret = eClaim.FirstOrDefault<Claim>();
if (ret != null)
{
//create the string we're going to send to the Azure queue: claim, value, and display name
//note that I'm using "#" as delimiters because there is only one parameter, and CASI Kit
//uses ; as a delimiter so a different value is needed. If ; were used CASI would try and
//make it three parameters, when in reality it's only one
string qValue = ret.ClaimType + "#" + ret.Value + "#" + "Email";
//create the connection to Azure and upload
//create an instance of the control
AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();
//set the properties to retrieve data; must configure cache properties since we're using it programmatically
//cache is not actually used in this case though
cfgCtrl.WcfUrl = AzureCCP.SVC_URI;
cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;
cfgCtrl.MethodName = "AddClaimsToQueue";
cfgCtrl.MethodParams = qValue;
cfgCtrl.ServerCacheTime = 10;
cfgCtrl.ServerCacheName = ret.Value;
cfgCtrl.SharePointClaimsSiteUrl = this.Page.Request.Url.ToString();
//execute the method
bool success = cfgCtrl.ExecuteRequest();
if (success)
{
//if it worked tell the user
statusLbl.Text = "<p>Your information was successfully added. You can now contact any of " +
"the other Partner Members or our Support staff to get access rights to Partner " +
"content. Please note that it takes up to 15 minutes for your request to be " +
"processed.</p>";
}
else
{
statusLbl.Text = "<p>There was a problem adding your info to Azure; please try again later or " +
"contact Support if the problem persists.</p>";
}
}
}
}
}
catch (Exception ex)
{
statusLbl.Text = "There was a problem adding your info to Azure; please try again later or " +
"contact Support if the problem persists.";
Debug.WriteLine(ex.Message);
}
}
}
对代码进行一下简单梳理:首先确保请求通过身份验证;如果通过,则将按钮添加到页面中,并且为按钮的单击事件添加事件处理程序。在按钮的单击事件处理程序中,我获得对当前用户的 IClaimsPrincipal 引用,然后查看用户的声明集。针对声明集运行 LINQ 查询以查找电子邮件声明,即我的 SPTrustedIdentityTokenIssuer 的标识声明。如果我找到电子邮件声明,则使用声明类型、声明值和声明的友好名称创建一个串联字符串。重申一下,此应用场景对此并没有严格要求,但是因为我想让其适用于更普遍的应用场景,所以我才这样编码。该串联字符串是 WCF 上用于将数据添加到 Azure 队列的方法的值。然后,我创建自定义 CASI 工具包组件的实例,并将其配置为调用将数据添加到队列的 WCF 方法,然后调用 ExecuteRequest 方法实际发送数据。
如果我收到指示已成功将数据添加到队列的响应,随后我会告知用户;否则,让用户知道发生了问题且需要稍后再试。在实际的应用场景中,我会有更多的错误日志记录,以便跟踪到底发生了什么以及为什么发生。尽管这样,CASI 工具包还是会将所有错误信息写入 SPMonitoredScope 中的 ULS 日志,所以它对请求做的任何事情都将具有一个唯一的相关 ID,通过该 ID 我们可以查看所有与请求相关的活动。这样我实际上便处于一个相当不错的状态了。
好的,现在我们已经了解了所有相关组件,并且已经展示了数据是如何添加到 Azure 队列中的,以及从那里被辅助进程调出并添加到表存储中。这就是终极目标,因为现在我们可以讨论自定义声明提供程序了。它将使用 CASI 工具包调用和查询我使用的 Azure 表存储。让我们了解下自定义声明提供程序最有趣的方面。
首先让我们来了解几个类级别的属性:
//the WCF endpoint that we'll use to connect for address book functions
//test url: https://az1.vbtoys.com/AzureClaimsWCF/AzureClaims.svc
//production url: https://spsazure.vbtoys.com/AzureClaims.svc
public static string SVC_URI = "https://spsazure.vbtoys.com/AzureClaims.svc";
//the identity claimtype value
private const string IDENTITY_CLAIM =
"https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
//the collection of claim type we support; it won't change over the course of
//the STS (w3wp.exe) life time so we'll cache it once we get it
AzureClaimProvider.AzureClaims.ClaimTypeCollection AzureClaimTypes =
new AzureClaimProvider.AzureClaims.ClaimTypeCollection();
首先,我使用一个常量引用 CASI 工具包应连接到的 WCF 终结点。您将注意到我同时具有测试终结点和生产终结点。当您以编程方式使用 CASI 工具包时(与在我们的自定义声明提供程序中一样),您总是需要告诉它要交流的 WCF 终结点位于何处。
接下来,正如我之前所说的,我将使用电子邮件声明作为我的标识声明。因为我要在提供程序中多次引用它,所以我就在类级别插入了一个常量。
最后,我具有了 AzureClaimTypes 集合。我在本系列的第 2 部分中已解释过,为什么我要使用集合,我只是在类级别将其存储在这里,以便每次调用 FillHierarchy 方法时无需重新获取该信息。对 Azure 的调用成本比较高,所以我尽可能减少调用的次数。
下面是接下来的代码段:
internal static string ProviderDisplayName
{
get
{
return "AzureCustomClaimsProvider";
}
}
internal static string ProviderInternalName
{
get
{
return "AzureCustomClaimsProvider";
}
}
//*******************************************************************
//USE THIS PROPERTY NOW WHEN CREATING THE CLAIM FOR THE PICKERENTITY
internal static string SPTrustedIdentityTokenIssuerName
{
get
{
return "SPS ACS";
}
}
public override string Name
{
get
{
return ProviderInternalName;
}
}
我之所以给出这段代码,是因为既然我的提供程序将发布标识声明,它就必须是 SPTrustedIdentityTokenIssuer 的默认提供程序。解释如何执行此过程则超出了本文的范围,但我在我的博客的其他地方有介绍。执行该过程最需要注意的就是您必须在用于提供程序的名称和用于 SPTrustedIdentityTokenIssuer 的名称之间具有强关系。我用于 ProviderInternalName 的值是必须插入 SPTrustedIdentityTokenIssuer 的 ClaimProviderName 属性的名称。而且,我在为用户创建标识声明时需要使用 SPTrustedIdentityTokenIssuer 的名称。所以我创建了一个名为“SPS ACS”的 SPTrustedIdentityTokenIssuer 并且将其添加到 SPTrustedIdentityTokenIssuerName 属性。这就是我在这里对这些值进行编码的原因。
因为我在此提供程序中并没有进行任何声明扩充,所以我未编写任何代码重写 FillClaimTypes、FillClaimValueTypes 或 FillEntityTypes。我的下一段代码是 FillHierarchy,在其中我告知 SharePoint 我支持哪些声明类型。代码如下所示:
try
{
if (
(AzureClaimTypes.ClaimTypes == null) ||
(AzureClaimTypes.ClaimTypes.Count() == 0)
)
{
//create an instance of the control
AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();
//set the properties to retrieve data; must configure cache properties since we're using it programmatically
//cache is not actually used in this case though
cfgCtrl.WcfUrl = SVC_URI;
cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;
cfgCtrl.MethodName = "GetClaimTypes";
cfgCtrl.ServerCacheTime = 10;
cfgCtrl.ServerCacheName = "GetClaimTypes";
cfgCtrl.SharePointClaimsSiteUrl = context.AbsoluteUri;
//execute the method
bool success = cfgCtrl.ExecuteRequest();
if (success)
{
//if it worked, get the list of claim types out
AzureClaimTypes =
(AzureClaimProvider.AzureClaims.ClaimTypeCollection)cfgCtrl.QueryResultsObject;
}
}
//make sure picker is asking for the type of entity we return; site collection admin won't for example
if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))
return;
//at this point we have whatever claim types we're going to have, so add them to the hierarchy
//check to see if the hierarchyNodeID is null; it will be when the control
//is first loaded but if a user clicks on one of the nodes it will return
//the key of the node that was clicked on. This lets you build out a
//hierarchy as a user clicks on something, rather than all at once
if (
(string.IsNullOrEmpty(hierarchyNodeID)) &&
(AzureClaimTypes.ClaimTypes.Count() > 0)
)
{
//enumerate through each claim type
foreach (AzureClaimProvider.AzureClaims.ClaimType clm in AzureClaimTypes.ClaimTypes)
{
//when it first loads add all our nodes
hierarchy.AddChild(new
Microsoft.SharePoint.WebControls.SPProviderHierarchyNode(
ProviderInternalName, clm.FriendlyName, clm.ClaimTypeName, true));
}
}
}
catch (Exception ex)
{
Debug.WriteLine("Error filling hierarchy: " + ex.Message);
}
这里,我要查看是否已获得了我支持的声明类型的列表。如果未获得,我会创建一个 CASI 工具包自定义控件的实例并调用 WCF 以检索声明类型;我通过对 WCF 类调用 GetClaimTypes 方法来执行此过程;如果返回了数据,那么我将数据插入我之前介绍的名为 AzureClaimTypes 的类级别变量,然后将其添加到我支持的声明类型的层次结构中。
我们将介绍的下一个方法是 FillResolve 方法。FillResolve 方法有两种不同的签名,因为它们执行不同的任务。在第一种情况中,我们具有特定的带值和类型的声明,SharePoint 只是想验证其是否有效。在第二种情况中,用户在 SharePoint 键入控件中键入了一些值,所以其实际上与执行声明搜索相同。正因为如此,我将区别对待这两种情况。
在具有特定声明并且 SharePoint 想要验证值的情况中,调用我编写的名为 GetResolveResults 的方法。在该方法中,我传递发出请求的 Uri 以及 SharePoint 寻求验证的声明类型和声明值。GetResolveResults 如下所示:
//Note that claimType is being passed in here for future extensibility; in the
//current case though, we're only using identity claims
private AzureClaimProvider.AzureClaims.UniqueClaimValue GetResolveResults(string siteUrl,
string searchPattern, string claimType)
{
AzureClaimProvider.AzureClaims.UniqueClaimValue result = null;
try
{
//create an instance of the control
AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();
//set the properties to retrieve data; must configure cache properties since we're using it programmatically
//cache is not actually used in this case though
cfgCtrl.WcfUrl = SVC_URI;
cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;
cfgCtrl.MethodName = "ResolveClaim";
cfgCtrl.ServerCacheTime = 10;
cfgCtrl.ServerCacheName = claimType + ";" + searchPattern;
cfgCtrl.MethodParams = IDENTITY_CLAIM + ";" + searchPattern;
cfgCtrl.SharePointClaimsSiteUrl = siteUrl;
//execute the method
bool success = cfgCtrl.ExecuteRequest();
//if the query encountered no errors then capture the result
if (success)
result = (AzureClaimProvider.AzureClaims.UniqueClaimValue)cfgCtrl.QueryResultsObject;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
return result;
}
这里我创建了自定义 CASI 工具包控件的实例,然后对 WCF 调用 ResolveClaim 方法。该方法使用两个参数,所以我以分号分隔值的方式传递这两个参数(因为那是 CASI 工具包区分不同参数值的方法)。然后执行请求,如果找到匹配项,则返回单个 UniqueClaimValue;否则返回值将为空。我的 FillResolve 方法的代码如下所示:
protected override void FillResolve(Uri context, string[] entityTypes, SPClaim resolveInput, List<PickerEntity> resolved)
{
//make sure picker is asking for the type of entity we return; site collection admin won't for example
if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))
return;
try
{
//look for matching claims
AzureClaimProvider.AzureClaims.UniqueClaimValue result =
GetResolveResults(context.AbsoluteUri, resolveInput.Value,
resolveInput.ClaimType);
//if we found a match then add it to the resolved list
if (result != null)
{
PickerEntity pe = GetPickerEntity(result.ClaimValue, result.ClaimType,
SPClaimEntityTypes.User, result.DisplayName);
resolved.Add(pe);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
所以我首先检查以确保请求是对用户声明的请求,因为那是我的提供程序返回的唯一声明类型。如果请求不是对用户声明的请求,那么弃置不管。接下来,如果返回的是非空的结果,则调用我的方法解析声明,并进行处理。处理过程中,我调用另一个我编写的名为 GetPickerEntity 的自定义方法。这里,我传递声明类型和值以创建标识声明,然后我可以将返回到传递的 PickerEntity 实例列表的该 PickerEntity 添加到我的方法。我不打算详细介绍 GetPickerEntity 方法,因为这篇文章的内容已经相当多了,并且在我的其他博文中已介绍过如何执行此过程。
现在让我们讨论另一个 FillResolve 方法。正如我之前所述,它基本上就是一个搜索,所以在这里我基本上将结合使用 FillResolve 和 FillSearch 方法。这两种方法将调用我编写的名为 SearchClaims 的自定义方法,如下所示:
private AzureClaimProvider.AzureClaims.UniqueClaimValueCollection SearchClaims(string claimType, string searchPattern,
string siteUrl)
{
AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =
new AzureClaimProvider.AzureClaims.UniqueClaimValueCollection();
try
{
//create an instance of the control
AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();
//set the properties to retrieve data; must configure cache properties since we're using it programmatically
//cache is not actually used in this case though
cfgCtrl.WcfUrl = SVC_URI;
cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;
cfgCtrl.MethodName = "SearchClaims";
cfgCtrl.ServerCacheTime = 10;
cfgCtrl.ServerCacheName = claimType + ";" + searchPattern;
cfgCtrl.MethodParams = claimType + ";" + searchPattern + ";200";
cfgCtrl.SharePointClaimsSiteUrl = siteUrl;
//execute the method
bool success = cfgCtrl.ExecuteRequest();
if (success)
{
//if it worked, get the array of results
results =
(AzureClaimProvider.AzureClaims.UniqueClaimValueCollection)cfgCtrl.QueryResultsObject;
}
}
catch (Exception ex)
{
Debug.WriteLine("Error searching claims: " + ex.Message);
}
return results;
}
在此方法中,正如您在本文的其他地方见到的一样,我只是创建了自定义 CASI 工具包控件的一个实例。我对 WCF 调用 SearchClaims 方法并传递我要搜索的声明类型、我要在该声明类型中找到的声明值,以及返回记录的最大数量。您可以回顾一下本系列的第 2 部分,SearchClaims 只不过是对传入的搜索模式执行 BeginsWith,所以如果有很多用户,很容易就会超过 200 条结果。然而,200 是人员选取器显示的匹配项的最大数量,所以这就是我想要的。如果您真的认为用户可以滚动查看超过 200 条结果,那么我在这里告诉您,这是不可能的。
现在我们有了 UniqueClaimValues 的集合,我们来看看如何在自定义声明提供程序中使用我们的两个重写方法。首先,FillResolve 方法如下所示:
protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List<PickerEntity> resolved)
{
//this version of resolve is just like a search, so we'll treat it like that
//make sure picker is asking for the type of entity we return; site collection admin won't for example
if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))
return;
try
{
//do the search for matches
AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =
SearchClaims(IDENTITY_CLAIM, resolveInput, context.AbsoluteUri);
//go through each match and add a picker entity for it
foreach (AzureClaimProvider.AzureClaims.UniqueClaimValue cv in results.UniqueClaimValues)
{
PickerEntity pe = GetPickerEntity(cv.ClaimValue, cv.ClaimType, SPClaimEntityTypes.User, cv.DisplayName);
resolved.Add(pe);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
它只是调用 SearchClaims 方法,对于它返回的每个结果(如果有),都创建一个新的 PickerEntity 并将其添加到它们(传递给重写)的列表。随后它们将全部显示在 SharePoint 的键入控件中。FillSearch 方法按如下所示使用它:
protected override void FillSearch(Uri context, string[] entityTypes, string searchPattern, string hierarchyNodeID, int maxCount, SPProviderHierarchyTree searchTree)
{
//make sure picker is asking for the type of entity we return; site collection admin won't for example
if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))
return;
try
{
//do the search for matches
AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =
SearchClaims(IDENTITY_CLAIM, searchPattern, context.AbsoluteUri);
//if there was more than zero results, add them to the picker
if (results.UniqueClaimValues.Count() > 0)
{
foreach (AzureClaimProvider.AzureClaims.UniqueClaimValue cv in results.UniqueClaimValues)
{
//node where we'll stick our matches
Microsoft.SharePoint.WebControls.SPProviderHierarchyNode matchNode = null;
//get a picker entity to add to the dialog
PickerEntity pe = GetPickerEntity(cv.ClaimValue, cv.ClaimType, SPClaimEntityTypes.User, cv.DisplayName);
//add the node where it should be displayed too
if (!searchTree.HasChild(cv.ClaimType))
{
//create the node so we can show our match in there too
matchNode = new
SPProviderHierarchyNode(ProviderInternalName,
cv.DisplayName, cv.ClaimType, true);
//add it to the tree
searchTree.AddChild(matchNode);
}
else
//get the node for this team
matchNode = searchTree.Children.Where(theNode =>
theNode.HierarchyNodeID == cv.ClaimType).First();
//add the match to our node
matchNode.AddEntity(pe);
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
在 FillSearch 中,我再次调用 SearchClaims 方法。对于每个返回的 UniqueClaimValue(如果有),我查看是否已将声明类型添加到结果层级结构节点。再次重申,在本例中,我始终只返回一种声明类型(电子邮件),但是我以可扩展的方式编写代码,以便以后您可以使用更多的声明类型。所以如果层次结构节点不存在,就添加它;如果存在,就找到它。我使用从 UniqueClaimValue 创建的 PickerEntity 并将其添加到层次结构节点。我想这里就这么多的内容了。
我不打算详细介绍 FillSchema 方法或每个自定义声明提供程序都必须具有的所有四种布尔属性重写,因为它们在此应用场景中并没有什么特别的,我已经在其他的博客文章中介绍过它们的一些基础知识。我也不打算介绍用于注册此自定义声明提供程序的功能接收器,同样是因为对于此项目来说没有什么特别的,并且在其他文章中已对其进行了介绍。编译之后,您只需确保在场中每台服务器的全局程序集缓存中注册了自定义声明提供程序的程序集以及自定义 CASI 工具包组件,并且您需要配置 SPTrustedIdentityTokenIssuer 将您的自定义的声明提供程序用作默认的提供程序(在本博客的其他地方已介绍)。
这是基本的端对端应用场景。当您在 SharePoint 网站中尝试添加新用户(实际上是电子邮件声明)时,首先调用自定义声明提供程序以获取支持声明类型的列表,当您在键入控件中键入值或使用人员选取器搜索值时亦是如此。在每种情况下,自定义声明提供程序都使用自定义 CASI 工具包控件对 Windows Azure 执行经过身份验证的调用以便与 WCF 对话,WCF 使用自定义数据类从 Azure 表存储检索数据。它返回结果,我们对其解包并向用户显示。通过这些,您便具有了可以原封不动使用的完整 SharePoint 和 Azure“现成 Extranet”解决方案,或者稍作修改以满足您的需求。本文随附有自定义 CASI 工具包组件、在 Azure 队列中注册用户的 Web 部件和自定义声明提供程序的所有源代码。希望能对您有所帮助,并且您可以开始设想如何结合使用这些单独的服务来创建解决您自己问题的解决方案。下面是最终解决方案的一些屏幕截图:
匿名用户的根网站:
以下是它在您经过身份验证后的外观;请注意 Web 部件现在显示“请求成员身份”(Request Membership) 按钮:
下面是人员选取器实际应用的示例(在搜索以“sp”开头的声明值之后):
这是一篇本地化的博客文章。请访问 The Azure Custom Claim Provider for SharePoint Project Part 3 以查看原文