Hello World
Spiga

深入Atlas系列:探究Application Services(1) - Profile Service分析与使用

2006-11-02 23:43 by 老赵, 3330 visits
  ASP.NET AJAX提供了Profile Service,允许开发人员异步地从服务器端访问Profile信息。从RTM开始,客户端的Profile Service还提供了对于Profile Group的支持,因此可以说已经相当成熟了。那么对于Profile Service的细节,是否大家都了解了呢?从ScriptManager的使用上来看,ProfileService是能够扩展的,那么应该如何扩展呢?细心的朋友们应该也发现了,在web.config中也增加了对于Profile Service的配置,那么这些配置应该如何使用呢?

在这篇文章里,我们将一起来讨论Profile Service使用方式的一些细节。


1、ASP.NET Profile基础

这部分简单地描述了ASP.NET Profile的使用,如果已经了解ASP.NET Profile的朋友就可以跳过这部分了。

在ASP.NET中,可以使用Profile,只需在web.config中进行定义即可。例如:
<system.web>
    
<anonymousIdentification enabled="true" />
    
<profile enabled="true" defaultProvider="SqlProvider" >
        
<providers>
            
<add
                
name="SqlProvider"
                connectionStringName
="SqlServices"
                applicationName
="ProfileBaseApplication"
                type
="System.Web.Profile.SqlProfileProvider" />
        
</providers>

        
<properties>
            
<add name="ZipCode" allowAnonymous="true" />
            
<add name="RecentSearchList"
                type
="System.Collections.Specialized.StringCollection"
                serializeAs
="Xml"
                allowAnonymous
="true" />
        
</properties>
    
</profile>
</system.web>

在运行时,则可以通过当前Context的Profile属性获得当前用户的Profile信息,Profile属性是一个继承ProfileBase的ProfileCommon类实例,根据web.config的中的定义提供了强类型的属性访问。例如:
public void Page_Load(object sender, EventArgs e)
{
    
this.Profile.ZipCode = "...";
}

在web.config中也可以定义ProfileGroup。例如:
<properties>
    
<add name="ZipCode" />
    
<group name="Address">
        
<add name="Street" />
        
<add name="City" />
        
<add name="State" />
        
<add name="CountryOrRegion" />
    
</group>
</properties>

这样就定义了一个ProfileGroup,它会被定义成一个ProfileGroupBase的子类ProfileGroupXXXX(例如上面的定义则是ProfileGroupAddress),然后这个ProfileGroup也提供了相应的强类型属性。这样,就可以通过如下的方式访问到:
public void Page_Load(object sender, EventArgs e)
{
    
this.Profile.Address.City = "...";
}

Profile是一个非常容易使用的东西,我们能够为其赋值,它能够使用指定Provider自动地进行保存。另外在web.config文件中,我们也能够为Profile的的各项值定义各种属性,例如是否能够被匿名用户使用,是否只读,甚至让ProfileCommon类继承自定义的Profile子类。详细信息,请感兴趣的朋友们参考MSDN相关章节。


2、配置ASP.NET AJAX Profile Service

查看ASP.NET AJAX提供的配置文件,在<configSections />可以看到如下定义:
<configSections>
    
<sectionGroup name="microsoft.web" type="Microsoft.Web.Configuration.MicrosoftWebSectionGroup, Microsoft.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
        
<sectionGroup name="scripting" type="Microsoft.Web.Configuration.ScriptingSectionGroup, Microsoft.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
            
<sectionGroup name="webServices" type="Microsoft.Web.Configuration.ScriptingWebServicesSectionGroup, Microsoft.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
                
<section name="jsonSerialization" type="Microsoft.Web.Configuration.ScriptingJsonSerializationSection, Microsoft.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" />
                
<section name="profileService" type="Microsoft.Web.Configuration.ScriptingProfileServiceSection, Microsoft.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" />
                
<section name="authenticationService" type="Microsoft.Web.Configuration.ScriptingAuthenticationServiceSection, Microsoft.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" />
            
</sectionGroup>
        
</sectionGroup>
    
</sectionGroup>
</configSections>

请注意加粗的部分,它说明在web.config中,我们可以配置Profile Service的一些属性。根据分析代码(事实上从web.config的注释中也能得察一二),可以得到配置的使用方式。例如:
<profileService enabled="true" 
    writeAccessProperties
="ZipCode, Address.Street" 
    readAccessProperties
="Address.State" />

如果将enabled设为false,则在使用Profile Service时会抛出异常。writeAccessProperties和readAccessProperties属性(在这里感谢Cat Chen的提醒,请注意在Beta1版本中,web.config中的注释是错误的)分别定义了可以使用Profile Service设置和获取的属性列表。由于System.Web.UI.WebControls.StringArrayConverter的作用,能够使逗号分割的字符串与字符串数组之间进行转换。另外,能够使用“.”来表述Group中的某个Profile信息,例如“Address.State”则表示了Address这个Group下的State信息。

请注意,虽然定义了writeAccessProperties和readAccessProperties,但是从客户端设法设置或读取“不被允许”的Profile属性也不会出现错误,只是设置或者读取无法成功而已。


3、使用Profile Service读取Profile信息

从客户端获取Profile信息的主要逻辑如下:
  1. 调用Sys.Services.ProfileService.load(propertyNames, loadCompletedCallback, failedCallback, userContext)。
  2. 如果propertyNames为空,则调用服务器端GetAllPropertiesForCurrentUser方法,否则使用_clonePropertyNames函数剔出propertyNames中重复的属性名,然后调用服务器端_GetPropertiesForCurrentUser方法。
  3. 服务器端根据得到的参数,从Profile信息中提取信息并构造出一个Dictionary<string, object>数组。
  4. 如果成功,则根据服务器端返回对象,调用_unflattenProperties方法将该对象转换为Profile属性以及Profile Group,最后调用loadCompletedCallback回调函数。
  5. 如果失败,调用failedCallback函数。
  需要注意的是,如果需要加载所有的Profile,在第1步中则必须使用null作为第一个参数,如果使用了一个空数组,则不会加载任何Profile属性。

关于第2步中的方法访问,ASP.NET AJAX中的Application Services都是基于ASP.NET AJAX的客户端Web Services访问能力的。在默认情况下Profile Service会访问“ScriptServices/Microsoft/Web/Profile/ProfileService.asmx”,由于不存在这个文件,ASP.NET AJAX将会查找Microsoft.Web.Profile.ProfileService类并调用该类的方法,这个就是“Assembly-based Web Services Access”。不过令人扼腕的是,虽然在当前版本的ASP.NET AJAX中还保留了这个机制,但是根据“白皮书”和可靠来源基本上可以确定这个功能会在下一个版本中被移除。这让我感到非常不可思议。个人认为,ASP.NET AJAX的一个重要特点,就是提供了一个能够扩展的模型。对于客户端来说,无论Control,Action,Validator乃至Behavior,几乎方方面面都能有规可循地进行扩展。很自然,服务器端也是,可能ASP.NET AJAX服务器端中最典型的应用就是Control Tookit了。其良好的复用性让人可谓眼前一亮。既然封装成了一个组件,则分发和部署则成为了一件重要的事情。许多ASP.NET AJAX组件必须通过客户端和服务器端的配合,然后使用客户端访问服务器端的Web Services方法来执行。如果需要在程序集中进行输出Javascript,则可以使用内嵌资源。但是如果客户端需要访问的Web Services文件是独立于程序集之外的话,那么还是增加了分发和部署的难度,甚至在开发与维护方面也变得需要管理多个对象,非常麻烦。而“Assembly-based Web Services Access”就能很好地解决这个问题。这样优秀的特性为什么会被取消?

在第4步中,从服务器端得到的数据是以如下JSON形式出现的:
{
    
"ZipCode" : "...",
    
"Address.City" : "...",
    
"Address.State" : "..."
}

Profile Group也是通过“.”分割来表示的。很自然,如果在load方法的第一个参数需要传递一个Profile Group中的一个属性时,也需要使用这样的表示方法。

另外,loadCompletedCallback和failedCallback函数的签名分别如下:
// propertyNumber:成功加载的Profile数量,同一个Profile Group下的属性会分别计算
//
 userContext:调用load时传入的userContext
//
 methodName:方法名,值为"Sys.Services.ProfileService.load"
function loadCompletedCallback(propertyNumber, userContext, methodName)
{
    ...
}

// error:错误对象,存放了错误信息
//
 userContext:调用load时传入的userContext
//
 methodName:方法名,值为"Sys.Services.ProfileService.load"
function failedCallback(error, userContext, methodName)
{
    ...
}

事实上,这两个回调函数都可以在调用load时不指定,这样就会使用默认的回调函数和默认的userContext,它们可以通过以下方法设置:
Sys.Services.ProfileService.set_loadCompleteCallback(callback);
Sys.Services.ProfileService.set_defaultFailedCallback(callback);

在load方法调用成功后,就可以在客户端获得使用Profile的值了,例如:
var zipCode = Sys.Services.ProfileService.properties.ZipCode;
var city = Sys.Services.ProfileService.properties.Address.City;
var state = Sys.Services.ProfileService.properties.Address.State;



4、使用Profile Service设置Profile信息

从客户端设置Profile信息的主要逻辑如下:
  1. 调用Sys.Services.ProfileService.save(propertyNames, saveCompletedCallback, failedCallback, userContext) 。
  2. 调用_flattenProperties,根据propertyNames将Sys.Services.ProfileService.properties中的信息变为JSON形式。如果propertyNames为null,则表示保存所有Profile属性。
  3. 调用服务器端SetPropertiesForCurrentUser方法,服务器端会将JSON字符串转变为IDictionary<string, object>,并保存在Profile中。
  4. 如果成功,则调用saveCompletedCallback回调函数。
  5. 如果失败,则调用failedCallback回调函数。

在第2步中,如果需要表示Profile Group中的属性,依旧通过“.”来分割。如果需要修改Profile信息,直接修改properties对象即可。如果需要创建Profile Group,则需要使用到客户端的Sys.Services.ProfileGroup类。例如:
Sys.Services.ProfileService.properties.ZipCode = "...";

if(!Sys.Services.ProfileService.properties.Address)
{
      Sys.Services.ProfileService.properties.Address 
= new Sys.Services.ProfileGroup();
}
Sys.Services.ProfileService.properties.Address.City 
= "...";

如果某Profile属性为复杂类型,则使用JSON形式在客户端赋值即可。

至于回调函数的签名,和load一模一样,只是methodName的值变为了"Sys.Services.ProfileService.save"。



在这篇文章中,我们简单讨论了ASP.NET AJAX的使用方法。在下一篇文章中,我们将一起来看一下配合ScriptManager来自定义Profile Service。
Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

24 条回复

  1. 老赵
    admin
    链接

    老赵 2006-11-02 23:50:00

    不伦不类的文章……

  2. TerryLee
    *.*.*.*
    链接

    TerryLee 2006-11-02 23:50:00

    唉,最近要研究的东西好多啊……

    项目组也忙了起来-_-

  3. 老赵
    admin
    链接

    老赵 2006-11-02 23:52:00

    @TerryLee
    哎,是阿,快神经衰弱了……
    培训准备得如何了阿?不知道模版之类的什么时候会有……

  4. TerryLee
    *.*.*.*
    链接

    TerryLee 2006-11-02 23:54:00

    @Jeffrey Zhao
    还没准备呢,白天项目忙,晚上写点东西就没时间了:)

    模版问问田兄吧,这几天我也没顾得上问

  5. 老赵
    admin
    链接

    老赵 2006-11-02 23:56:00

    @TerryLee
    呵呵,坚持……

  6. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2006-11-03 01:13:00

    这个东西你有没有试运行过?据我所知,这段写法不会导致编译错误,但会导致运行错误:
    <profileService enabled="true"
    setProperties="ZipCode, Address.Street"
    getProperties="Address.State" />

    而且,正如我之前所说,要用fiddler才能知道错误,而错误的原因你用reflector打开CTP和Beta1的代码看看就知道了,CTP是这样的:
    Microsoft.Web.Configuration.ProfileServiceSection {
    public string[] GetProperties {get;set;}
    public string[] SetProperties {get;set;}
    }
    然而Beta1是这样的:
    Microsoft.Web.Configuration.ScriptingProfileServiceSection {
    public string[] ReadAccessProperties {get;set;}
    public string[] WriteAccessProperties {get;set;}
    }

    然而在Beta1自动为你项目添加的web.config中,还是按老的写法来提供注释,这显然就是一个巨大的bug。

    我发现这个漏洞以后,准备再积累一些ProfileService的使用经验之后就写出来的,既然你写了我就可以懒得写啦,哈哈……仅仅做bug report。我写东西很慢很慢,因为总是喜欢一般对照着MSDN和Reflector看,看看MSDN哪里又在胡乱吹了,然后一边做实验验证事实。

  7. 老赵
    admin
    链接

    老赵 2006-11-03 01:35:00

    @Cat Chen
    阿哈,你仔细的。我本来准备看Reflector里到底写成了什么样,但是一看注释里有就轻心了,哈哈。
    其实代码里是ReadAccessPropeties或getProperties无关紧要,关键的是属性上面的CustomAttribute是[ConfigurationProperty("readAccessProperties", DefaultValue=null)]还是 [ConfigurationProperty("getProperties", DefaultValue=null)]。
    我改过来了。:)


  8. 阿不
    *.*.*.*
    链接

    阿不 2006-11-03 08:39:00

    简单的讲,就是提供给客户端调用的两个内置的WebService,Profile和Authentication,这两个WebService都定义在Microsoft.Web.Extension.dll。如果需要扩展,我们可以重写两个WebService,然后在ScriptManager中指定就行了,但是没法从内置的那两个继承下来,它们是被定义为internal sealed的类。

  9. 老赵
    admin
    链接

    老赵 2006-11-03 10:06:00

    @阿不
    继不继承倒无所谓,关键是取消了Assembly-based Web Services Access让我很无语。

  10. Dflying Chen
    *.*.*.*
    链接

    Dflying Chen 2006-11-03 11:36:00

    使用Profile Service读取Profile信息时候需要在ScriptManager中声明什么么?
    或者,需不需要在页面中添加ProfileService控件?

  11. 老赵
    admin
    链接

    老赵 2006-11-03 11:45:00

    @Dflying Chen
    不需要的,只要在web.config里enabled="true"就可以了,记得默认是false。

  12. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2006-11-03 12:41:00

    @Jeffrey Zhao
    是啊,关键看Attribute,不过应该不会有人用两个名字命名同一样东西吧,这是一条很基本的代码书写规范啊。

  13. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2006-11-03 12:50:00

    @阿不
    不能继承要完全重做是很麻烦的,如果你需要做一个兼容任意Profile配置的WebService的话。

    当你想扩展的时候,你就会发现问题了——我们平时使用的更新方法名为:
    public int SetPropertiesForCurrentUser(IDictionary<string, object> values)
    参数是一个字典,string是Property的名称,而object是Property的值。但Property的实值不是object啊,这个Atlas的JSON转换器可不管,给你的就是object。而且这个object不是转换好的object,而是Jeffrey之前面某篇WebService解释中说到的嵌套List<>/Dictionary<>。

    然后,ProfileService根据web.config中声明某一个Property的类型(例如MyComplexClass)来完成最终的转换,这个转换用的自然也是internal的方法。如果你自己编写一个ProfileService,你就要自己做这些转换,显然不是简单的事情。

  14. 老赵
    admin
    链接

    老赵 2006-11-03 13:53:00

    @Cat Chen
    其实为了向前兼容的话,这样的事情也是必须作的,不过现在的ASP.NET AJAX可不管这些。

    至于自己写Profile Service时,拿到的value参数的确是嵌套的IDictionary和IList,但是可以使用ASP.NET AJAX里提供的JavaScriptSerializer类来转换,这个类是我们在服务器端进行序列化/反序列化的唯一入口,提供的功能已经有很多了,完全足够,呵呵。

  15. 老赵
    admin
    链接

    老赵 2006-11-03 13:57:00

    public T ConvertToType<T>(object obj);
    可以使用JavaScriptSerializer类的这个方法,它的实现只有一行代码:
    public T ConvertToType<T>(object obj)
    {
      return (T) ObjectConverter.ConvertObjectToType(obj, typeof(T), this);
    }

    Profile Service就是用了ObjectConverter.ConvertObjectToType这个内部方法。

  16. 阿不
    *.*.*.*
    链接

    阿不 2006-11-03 19:20:00

    @Cat Chen
    @Jeffrey Zhao
    序列化和反序列化确实是一件很麻烦的事情,我一看它内部的序列化算法,头都大了。ObjectConverter.ConvertObjectToType这个方法仍然是内部的,我们无法调用到。最理想的就是它给我们提供一个做好基本功能的WebService抽象,供我们扩展。

  17. 阿不
    *.*.*.*
    链接

    阿不 2006-11-03 19:22:00

    @Jeffrey Zhao
    虽说它的序列化接口都对外公布了,但是似乎我们还需要做很多工作。看看ConvertObjectToTypeInternal这个方法就够吓人了。

  18. 老赵
    admin
    链接

    老赵 2006-11-03 19:28:00

    @阿不
    我基本上看完了这部分的代码,觉得该有的功能都已经有了,还有什么需要我们自己做呢?

  19. 阿不
    *.*.*.*
    链接

    阿不 2006-11-03 22:29:00

    @Jeffrey Zhao
    我是说,需要序列化,反序列化的的话,似乎还需要做其它的工作。

    另外,现有的功能不一定就能完全满足实际的需要,有的时候可能要对现有功能进行扩展和修改的。

  20. 老赵
    admin
    链接

    老赵 2006-11-04 00:12:00

    @阿不
    我总觉得现有的功能能满足几乎所有情况了,实在特殊的也没有办法……

  21. 老赵
    admin
    链接

    老赵 2006-11-04 03:30:00

    不过JavaScriptSerializer.ConvertToType<T>只有范型方法的确有缺陷:如果在程序里知道了一个Type,却不能简单地调用它,只能通过反射……

  22. 小蜗牛
    *.*.*.*
    链接

    小蜗牛 2006-11-04 08:54:00

    en ,书写规范ms真的很重要。

  23. hmx[未注册用户]
    *.*.*.*
    链接

    hmx[未注册用户] 2007-08-21 12:10:00

    如果我要更新在web.config文件中配置的profile数据值,也就是验证数据更新是否成功,要怎么才能知道啊。请帮忙一下,谢谢。

  24. 老赵
    admin
    链接

    老赵 2007-08-21 22:01:00

    @hmx
    对不起,我没有理解您的意思……

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我