会员+管理系统,第二部分:账号设置
下载源代码- 3.3 mb下载服务网站包- 5.6 mb下载演示数据样本- 181.1 KB 注意:本系列的每一篇文章对于数据服务可能有不同的数据模式。由于历史原因,它们位于不同的源代码控制分支上。因此,前面的文章可能不会与后面的文章同步。在其他文章中尝试重用数据服务和样例数据时应该小心。 注(2014-03-02):由于数据模式发生了变化,请将数据服务的早期版本(如果有的话)替换为当前版本,以便新版本工作。相关文章会员商店为ASP。NET Membership+ Management System,第一部分:BootstrappingMembership+ Management System,第三部分:查询智能介绍2。背景2.1全球化2.2内容缓存2.3 Web页面组成3。实现3.1全球化3.1.1资源适配器3.1.2要做的修改3.2客户端缓存3.3媒体处理3.3.1用户的“图标”3.3.2更新用户的图标图像3.3.3成员的照片3.4概要:成员属性3.5帐户信息3.5.1个人信息面板3.5.2密码管理面板3.6成员细节3.6.1加载用户详细信息3.6.1.1检索限制实体集操作没有详细的概要3.6.3详细概要经理3.6.3.1成员属性3.6.3.2成员照片3.6.3.3成员描述3.7用户沟通渠道3.7.1 3.7.2章概述KnockoutJS视图模型3.7.3 . net类型和JSON连接3.7.3.1服务器实体3.7.4通道3.7.4.1添加3.7.4.2修改3.7.4.3删除3.8备注4。连接数据服务5。历史1。会员管理系统的管理web应用程序的启动在会员管理系统,第一部分:Bootstrapping中有描述。这篇文章提供了一些关于全球化、媒体处理和会员个人信息管理的细节。2. 因特网是一个全球连接的网络,来自不同文化背景的人们可以共享它。如果互联网是全球化的,一个web应用程序可以从互联网中获得更多。在web应用程序中有多种实现全球化的方法。许多web站点使用不同的url来分隔不同语言的内容。这种全球化方案在这里称为显式全球化。这种方法有一些优点,比如它允许独立的面向语言的团队构建各种专用于特定语言的站点域,彼此之间的依赖性很小,等等,我们不应该在这里做完整的枚举。之一,这种方法的缺点是,它是困难和更昂贵的维护内容自网站分为语言从不同顶级域,并排很难做翻译,保持网站的内容同步一次部分改变了没有更高的人力资源支出。这里采用的方法称为隐式全球化。在web应用程序的隐式全球化模式中,所有受支持语言的内容url都保持不变。根据用户浏览上下文中的语言规范变量,将与特定语言对应的内容发送到用户的浏览器。其中一个明显的变量是大多数现代浏览器的内容语言偏好选项,它是由客户端系统自动设置的,用户可以在以后更改。选择将被编码到接受语言HTTP请求头中(见这里、这里和这里),并被发送到web服务器。我们开发了一种基于单一资源技术的轻量级全球化方法。系统的早期版本已经发布(例如,请参阅GTRM,如果reader试图下载和安装它,它很可能无法工作,因为系统的加载器目前不再受支持)。系统最新版本的文本资源编辑和翻译部分目前只在内部使用。然而,项目包含的组件可以用于检索文本块的各种语言定制uni-resource文本数据库构建使用该工具通过简单的api(见下面),以极高的速度,适合web页面包含许多小文本块,否则可能会拖垮站点的性能在被隐式地全球化。有人可能会问,为什么不简单地使用。net框架的资源系统,就像对客户端应用程序所做的那样?有几个原因:它选择特定于语言的文本块的默认API是基于的共享属性(即静态的或线程相关的),而不是d在web服务器上的线程池环境中的请求处理生命周期中,不使用排他锁。这样的锁将大大降低性能。这对于采用新版本4.0 . net运行时环境的异步范例的站点来说尤其如此,在这种环境中,一个异步处理程序可以将一个连续的执行流分割成在不同的线程上运行的部分(看,例如这里),在这种情况下锁定是不可能的。基于.net的非文件资源被编译为站点的程序集。这样就不可能在运行时更新文本。对于静态文本块来说还可以,但是动态web应用程序可以包含不断变化的内容。net资源的文件管理器在web服务器的高负载下是不可靠的,至少在。net的早期版本和我们测试过的用例中是如此。会发现抛出随机异常。另外,这些资源文件在运行时被锁定,因此不允许在不停止相应的web应用程序的情况下更新它们。它是不可移植的。例如,Linux上的MONO不支持。net资源;Windows RT有不同的方式编码资源,等等。2.2内容缓存^适当的缓存web内容可以显著提高web应用程序的性能。内容缓存可以在服务器端、客户端或两者结合进行。承载web应用程序的web服务器可以用于缓存不是很大的内容,或者作为整个响应的一部分,为每个请求重新生成这些内容的成本很高。如果一个完整的web内容整体大变化缓慢,叫做静态内容在以下,最好是在客户端缓存,因为它将节省内存消耗在服务器和带宽,否则可以被用来传送给客户经常变化的内容。大多数web浏览器可以保存在客户端和服务器端内容决定是否使用保存的副本,具体内容根据规则指定的Http协议,使用if - modified - since last - modified和具有的组合/ Etag头字段中包含相应的Http请求/响应。大多数web服务器可以处理基于静态内容的文件的客户端缓存,而不需要开发人员过多关注。这部分是因为大多数操作系统文件系统中的文件包含标准的元属性,比如mime类型和最后修改的时间,等等,web服务器可以在不知道应用程序上下文的情况下使用这些元属性来控制客户端缓存。不能很好处理的是从数据库中检索到的静态内容,因为这些内容所需的元信息没有一个标准。事实上,在许多系统中,某些或全部的元信息都是缺失的。不仅是web服务器,还包括Asp。Net framework不能访问需要的信息来处理客户端对以后静态内容的缓存。由于基于。net ActionFilterAttribute的类不能接受参数,OutputCacheAttribute只能很好地处理服务器端缓存。对于从数据库检索的静态内容的客户端缓存,可以发现操作级的显式处理更加灵活。会员+系统的数据模式(见这里)是如此设计,大多数静态和大型媒体数据对应最小的一组元属性,可用于控制客户端渲染和缓存:IconImage属性的一个实体UserAppMembers数据集有两个附带的属性,即IconMime IconLastModified,可用于控制客户端渲染和缓存的数据。IconLastModified的值从更新IconImage数据时使用的相应文件中检索。UserDetails数据集中的实体的Photo属性有两个伴随属性,即PhotoMime和PhotoLastModified,它们可以用于控制数据的客户端呈现和缓存。从更新照片数据所使用的相应文件中检索PhotoLastModified的值。数据服务的前端和负责输入和更新此类数据的web应用程序的构造使得元属性将自动从源数据(当前的文件)中检索(参见下面的实现部分)。2.3网页构成^这个部分是为那些刚接触Asp的人准备的。净Mvc框架。其他人可以跳过阅读。刚接触Asp的开发人员。但是有开发桌面或web表单应用程序知识的人可能会发现,在新的框架中似乎没有用户控制这样的东西。这实际上不是真的,“用户控件”是由部分视图表示的。使用部分视图有两种方法:直接包含部分视图来自@Html。部分(“ViewName”,模型)和@{Html。RenderPartial方法(“ViewName”模型);},前者生成字符串,后者返回void,因此调用方式不同。后者将从部分视图中生成的html直接写入响应输出流。通过以@Html的形式调用操作,间接而更灵活地包含部分视图。Action("ActionName", "ControllerName",…)和@ {Html。RenderAction("ActionName", "ControllerName",…);},在上面,前者生成一个字符串,而后者返回void,所以它的调用方式不同。后者将从部分视图中生成的html直接写入响应输出流。它不接受部分视图的名称,而是接受操作的名称和定义操作的控制器的可选名称。调用的操作负责选择哪个部分视图、所选视图如何初始化以及如何缓存各种输出。通常,动作的形式是隐藏。复制代码(ChildActionOnly) 公共ActionResult ActionName () { …部分视图选择和初始化… 返回PartialView(“PartialViewName viewmodel); } ChildActionOnly属性用于禁止从请求url直接调用操作。部分视图允许重构asp.net Mvc视图,以便一些公共部分可以在多个web页面中使用和重用,从而提高web页面的可维护性和一致性。3.资源适配器web应用程序使用ArchymetaMembershipPlusStores项目资源子目录下的Resources .cs文件中的类型和方法来隐式地检索单资源数据库中包含的文本资源。HTTP Accept-Language头字段中包含的语言首选项是按照ISO-639语言编码+ ISO-3166国家编码(请参阅此处)进行编码的,这与. net对语言进行分类的方式不同。map方法隐藏收缩,MapCultureInfo(string lan, out float weight) { int ipos = lan.IndexOf(';'); ipo == -1 ?lan.Trim():局域网。Substring (0, ipo) .Trim (); if (ipos == -1) 重量= 1.0度; 其他的 { 如果(! float.TryParse(局域网。Substring(ipos + 1).Trim(),输出重量 重量= 0.0度; } CultureInfo ci = null; 如果(cn = =“zh型”| | cn = =“zh-chs”| | cn = =“zh-hans”| | cn = =“应用”) ci = new CultureInfo("z - hans "); else if (cn = = " zh-cht " | | cn = =“- hant”| | cn = =“zh-tw”| | cn = =“zh-hk” | b| cn == "zh-sg") ci = new CultureInfo("zh-Hant"); else if (cn.StartsWith (zh型——“”) ci = new CultureInfo("z - hans "); 其他的 { bool失败= false; 试一试 { ci = new CultureInfo(cn); } 抓 { 失败= true; } 如果失败了,,cn.IndexOf(“-”)! = 1) { cn = cn。Substring (0, cn.IndexOf (' - ')); 试一试 { ci = new CultureInfo(cn); } 抓 { } } } 返回ci; } 用于映射二者。由于目前除了英语(美)和汉语之外,我们对其他语言没有足够详细的了解,这张地图在目前的开发阶段并不完善。项目中包含的单资源数据库中默认语言“en”的内容实际上更接近于“en- us”。但是,可以看到,以后可以很容易地对映射列表进行扩展。欢迎其他文化的读者在github.com上派生这个项目(在特性分支codeproject-2上),以提供额外的映射。当一个请求到达时,它的加权语言首选项列表(如果有的话)将与隐藏中指定的支持语言列表进行比较。add key="SupportedLanguages" value="en,zh-hans"/> appSettings>网络的节点。配置文件。第一场比赛将被视为响应中使用的语言。如果没有找到匹配,将使用默认语言“en”。在单资源存储器中,一个文本块及其在其他语言中的翻译列表由一个16字节的全局唯一标识符(guid)标识。resource eutils类的静态方法GetString的各种形式可以用于从单资源数据库中获取特定语言的文本块。最简单的就是隐藏。GetString(字符串resId,字符串defval = null) 其中文本guid的字符串形式,即resId是所述guid的十六进制编码值。属性中找不到要返回的文本时,使用defval指定该文本的默认值uni-resource数据库。该方法用于从数据库对应的单资源文件(即ShortResources)中检索小文本块。didxl ShortResources。web应用程序的App_Data子目录下的数据文件。BlockResources。didxl BlockResources。用于存储大型文本块的数据文件目前不包括在to project中。假设web页面首先是用英语编写的,全球化web页面的过程非常简单:找到任何需要全球化的文本块。将相应的文本块添加到单资源数据库中,该数据库将根据文本块的内容为其生成十六进制编码的guid。使用十六进制编码的guid作为它的第一个参数,使用原始文本作为它的第二个参数,通过调用GetString方法来替换原始文本块。例如,在Razor网页中,文本“主页”会被隐藏起来。Code@ResourceUtils副本。GetString (" cfe6e34a4c7f24aad32aa4299562f5b2”、“主页”) 与上一篇文章中描述的英语web应用程序相比,通过遵循上述规则,当前系统在本文和后续文章中是完全全球化的。对于那些对派生项目感兴趣的人来说,如果他们不能访问单资源输入工具,或者只对一种特定语言感兴趣,那么就不需要进一步将项目全球化。在前一种情况下,其他有工具的人可以为你做全球化。如果有足够的兴趣,我们可以选择重新发布这个工具的新版本。客户端缓存^ web应用程序中的所有控制器都派生自BaseController类,它包含两个管理客户端缓存控件的方法。隐藏,收缩,复制代码保护bool CheckClientCache(日期时间?lastModified、字符串Etag, 输出字符串StatusDescr) { StatusCode = 200; StatusDescr =“OK”; 如果(! EnableClientCache) 返回错误; 请求。header ["If-Modified-Since"]); bool HasEtag = !string.IsNullOrEmpty(Request.Headers[" if - non - match "]); 如果(!时间,,lastModified == null &&Etag = = null) 返回错误; DateTime吗?cacheTime =零; 如果(时间) cacheTime = DateTime.Parse ( Request.Headers [" if - modified - since "]) .ToUniversalTime (); HasEtag ?请求。标题(“具有”):空; 如果时间,,HasEtag) { if (lastModified != null && cacheTime.Value ! IsTimeGreater (lastModified.Value.ToUniversalTime ()),, OldEtag = = Etag) { StatusCode = 304; StatusDescr = "未修改"; 返回true; } } else if(时间) { if (lastModified != null && acheTime.Value ! IsTimeGreater (lastModified.Value.ToUniversalTime ())) { StatusCode = 304; StatusDescr = "未修改"; 返回true; } } else if (HasEtag) { if (OldEtag == Etag) { StatusCode = 304; StatusDescr = "未修改"; 返回true; } } 返回错误; } 用于检查是否存在有效的客户端缓存项并隐藏复制Codeprotected void SetClientCacheHeader(DateTime?LastModified、字符串Etag, CacheKind, bool ReValidate = true) { 如果(!LastModified == null &Etag = = null) 返回; cp = Response.Cache; ("max-age=" + 3600 * MaxClientCacheAgeInHours); 如果(重新验证) { cp.AppendCacheExtension(“must-revalidate”); cp.AppendCacheExtension(“proxy-revalidate”); } cp.SetCacheability (CacheKind); cp.SetOmitVaryStar(假); if (LastModified != null) cp.SetLastModified (LastModified.Value); cp.SetExpires (DateTime.UtcNow.AddHours (MaxClientCacheAgeInHours)); if (Etag != null) cp.SetETag (Etag); } 在可能的情况下设置客户端缓存。两种方法中使用的EnableClientCache属性与Web中的a配置设置相关。配置文件:隐藏复制代码保护bool EnableClientCache { 得到 { ["EnableClientCache"]; bool bval; 如果(string.IsNullOrEmpty(sval) || !TryParse (sval bval)) 返回错误; 其他的 返回bval; } } 它可以控制是否启用客户端缓存。客户端缓存可能使调试和测试变得困难。开发人员可以在开发期间关闭客户端缓存,并在使用相应的配置设置部署web应用程序时启用它。3.3媒体处理^这里的媒体是指网页上呈现的图像、视频和音频数据。基于文件的媒体处理相对容易。这里感兴趣的是如何处理从非文件数据源(如本文所强调的关系数据源)检索的媒体数据。3.3.1用户的“图标”^用户一旦登录,就有一个经过身份验证的唯一标识,可以直观地表示出来。最简单的就是用户的用户名。这是在我前面引用的文章中描述的系统初始化阶段为了简单起见所做的工作。然而,它可以变得更花哨。这是因为UserAppMembers数据集中的实体包含一个名为IconImage的字段,该字段可以包含用户为当前应用程序选择的图标图片。由于用户的“图标”可以在各种web页面中使用,最好为它定义一个可重用的部分视图。下面是最简单的一个,它包含一个用户的图标图片隐藏。复制Code@model MemberAdminMvc5.Models.UserIconModel @using Microsoft.AspNet.Identity @using Archymeta.Web.Security.Resources & lt; div类=“UserSmallIcon”比; @ if (! string.IsNullOrEmpty (Model.IconUrl)) { & lt; img src = "。内容(“~ /”+ Model.IconUrl)”/比; } 其他的 { & lt;跨类= " ion-android-contact "祝辞& lt; / span> } & lt; span> @Model.Greetings< / span> & lt; / div> 它包含在_UserIconPartial中。web应用程序的Views\Account子目录下的cshtml文件。这个部分视图由用户自己使用。在web应用程序的Views\Home子目录下还有另一个文件,文件名相同,显示给系统的其他成员。除了@Model之外,两者之间几乎没有什么区别。问候语用@Model代替。UserLabel在后面的视图中。部分视图处理用户可能没有上传图标的情况,在这种情况下,将显示默认图标图片。图:用户“图标”部分视图。视图模型UserIconModel被定义为隐藏复制Codepublic类UserIconModel { 公共字符串UserLabel { 得到; 设置; } 公共字符串的问候 { 得到; 设置; } 公共字符串IconUrl { 得到; 设置; } } 这个部分视图通常通过@Html.Action(…)包含,因为它不期望类型为UserIconModel的对象被包含并初始化在所有类型的视图模型中,这些视图模型与具有用户“图标”的web页面相关联。使用专用的公共动作动态地注入模型会更简单。例如,在_LoginPartial中。cshtml文件在视图\共享子目录下的web应用程序,用户“图标”通过隐藏被包含。复制Code@ {Html。RenderAction(“GenerateUserIcon”、“账户”);} 调用AccountController的GenerateUserIcon方法来设置部分视图:复制代码(ChildActionOnly) 公共ActionResult GenerateUserIcon () { var m = new Models.UserIconModel(); m。问候= User.Identity.GetUserName (); m。UserLabel = m.Greetings; var ci =用户。System.Security.Claims.ClaimsIdentity身份; string strIcon =(源自ci.Claims中的d 在d。类型= = CustomClaims.HasIcon 选择d.Value) .SingleOrDefault (); bool hasIcon; 如果(! string.IsNullOrEmpty (strIcon),, 保龄球TryParse(strIcon, out hasIcon)hasIcon) { m。IconUrl = "帐户/ GetMemberIcon吗?id = " + User.Identity.GetUserId (); } 返回PartialView(“_UserIconPartial”,m); } 它不检索图像,而是提供一个图像url,即m。图标url,该网页下一步可以下载。这个url调用AccountController类的GetMemberIcon方法,该方法返回二进制图像数据:收缩,复制代码(HttpGet) 公共异步Task< ActionResult>GetMemberIcon (string id) { 如果(string.IsNullOrEmpty (id)) { 如果(! Request.IsAuthenticated) 返回新的HttpStatusCodeResult(404,“未找到”); id = User.Identity.GetUserId (); } var rec = await MembershipContext.GetMemberIcon(id); if (rec == null<span lang="z -cn">| | string.IsNullOrEmpty (rec.MimeType) & lt; / span>) 返回新的HttpStatusCodeResult(404,“未找到”); int状态; 字符串statusstr; 检查客户端缓存(rec)LastModified rec.ETag, 输出状态,输出状态str); SetClientCacheHeader (rec。LastModified rec.ETag, HttpCacheability.Public); 如果(! bcache) 返回文件(rec。数据,rec.MimeType); 其他的 { 响应。StatusCode =状态; 响应。StatusDescription = statusstr; 返回内容(" "); } } 此次行动的主要责任人是韩某客户端缓存。它首先通过调用CheckClientCache方法来检查图像数据是否缓存在客户端,然后通过调用前面一节中描述的SetClientCacheHeader方法来尝试设置客户端缓存指令。图像数据的Etag是数据的base64编码的MD5哈希值:收缩,复制Codepublic类ContentRecord { 公共byte[]的数据 { 得到; 设置; } 公共字符串ETag { 得到 { 如果(Data == null ||数据。长度= = 0) 返回null; if (_etag == null) { var h = hashalgdma . create ("MD5"); _etag = Convert.ToBase64String (h.ComputeHash(数据)); } 返回_etag; } } private string _etag = null; 公共字符串MimeType { 得到; 设置; } 公共DateTime LastModified { 得到; 设置; } } 图像数据的检索委托给MembershipContext类的GetMemberIcon方法,该方法在MembershipPlusAppLayer45项目中定义。隐藏,复制Codepublic静态异步任务GetMemberIcon (string id) { UserAppMemberServiceProxy umsvc = new UserAppMemberServiceProxy(); var um =等待umsvc。LoadEntityByKeyAsync (Cntx ApplicationContext.App。ID, ID); if (um == null) 返回null; ContentRecord rec = new ContentRecord() rec.MimeType = um.IconMime; rec.LastModified = um.IconLastModified。HasValue吗? um.IconLastModified。价值:DateTime.MaxValue; rec.Data =等待umsvc。LoadEntityIconImgAsync (Cntx ApplicationContext.App。ID, ID); 返回矩形; } 由于IconImage声明为延时加载,图像数据加载分两个步骤:1)加载类型UserAppMember的实体模型,检索图像数据的元信息,然后加载图像数据。3.3.2更新用户的图标图像^用户的图标图像是在UpdateMemberIcon中设置或更新的。cshtml网页在视图\帐户子目录下隐藏收缩,复制Code@using Microsoft.AspNet.Identity; @using Archymeta.Web.Security.Resources; @ { ViewBag。标题= ResourceUtils.GetString(“a11249b2e553b45f53a9d1f5d0ac89ba”, “更新用户会员图标”); } {@section脚本 & lt; script> $(函数(){ $(" #提交”)。道具(“禁用”,真正的); 如果窗口。文件,,窗口。FileReader,, 窗口。文件列表,,window.Blob) { $ (" # IconImg”)[0]。addEventListener('change', function (evt) { var file = ev .target.files[0]; $ (' # IconLastModified ') .val (file.lastModifiedDate.toUTCString ()); var = new FileReader读者(); 读者。onload = function (e) { $ (" # imgPreview”)。attr(“src”,e.target.result); $(" #提交”)。道具(“禁用”,假); } reader.readAsDataURL(文件); },假); 其他}{ alert (' @ResourceUtils.GetString ( “0274 e2eeb63505510d4baab9f70dc418” "此浏览器不完全支持文件api ")'); } }); & lt; / script> } & lt; div类= "行"比; & lt; div类=“col-md-12”比; & lt; / div> & lt; / div> @using (Html。BeginForm(“UpdateMemberIconAsync”、“账户”, new {returnUrl = ViewBag。ReturnUrl}, FormMethod。Post, new {enctype = "multipart/form-data"}) { @Html.AntiForgeryToken () <input type="hidden" id="IconLastModified" name="IconLastModified" /> & lt; div类= "行"比; & lt; div class = " col-md-offset-2 col-md-10”比; & lt; div风格= "显示:inline-block”比; & lt;标签=“IconImg”比; @ResourceUtils。GetString("4673637028866e44d46b7c9760bf3a4c", "本地图标文件:") & lt; / label> <input type="file" id="IconImg" name="IconImg" class="form-control" /> & lt; div>& lt; / div> <input type="submit" name="submit" id="submit" class="btn btn-default" 值= " @ResourceUtils.GetString(“91412465 ea9169dfd901dd5e7c96dd9a”, “上传”)”/比; & lt; / div> & lt; div风格= "显示:inline-block;margin-left: 20 px;“比; & lt; img id = " imgPreview " src = " .content(“~ /账户/ GetMemberIcon ? id = " + User.Identity.GetUserId())”风格= " vertical-align:底部;“/比; & lt; / div> & lt; / div> & lt; / div> } 图:用户图标图像加载页面。在初始加载之后,页面向“IconImg”输入字段和旧图标图像添加一个JavaScript文件API侦听器,如果在页面的右侧显示了旧图标图像的话。用户选择一个图像文件后,隐藏的“IconLastModified”表单字段中会更新图像文件的最后修改日期,并在右侧显示新图像以供预览。当用户单击“Upload”按钮时,它调用AccountController的UpdateMemberIconAsync方法来进行更新。隐藏,收缩,复制代码(HttpPost) (授权) (ValidateAntiForgeryToken) [OutputCache(NoStore = true, Duration = 0)] 公共异步Task< ActionResult>UpdateMemberIconAsync(字符串returnUrl) { 如果请求。= null &&Request.Files。数比;0) { 如果(! Request.Files [0] .ContentType.StartsWith(“图像”)) 抛出新的异常(“内容不匹配!”); 字符串IconMime = Request.Files[0].ContentType; System.Nullable< DateTime>IconLastModified = 默认(System.Nullable< DateTime>); 如果(Request.Form.AllKeys.Contains(“IconLastModified”)) IconLastModified = DateTime.Parse (Request.Form [" IconLastModified "]); 先。[0].InputStream; int size = Request.Files[0].ContentLength; byte[] data = new byte[size]; strm。阅读(0,数据大小); 如果等待MembershipContext.UpdateMemeberIcon (User.Identity.GetUserId (), IconMime IconLastModified。值,数据) { 如果(string.IsNullOrEmpty (returnUrl)) 返回RedirectToAction(“指数”,“家”); 其他的 返回重定向(returnUrl); } } 返回视图(); } 其中,最后修改的日期,mime类型和图像数据被检索并传递给MembershipContext类的UpdateMemeberIcon方法:Hide复制代码公共静态异步任务字符串id,字符串mineType, DateTime lastModified, byte[] imgdata) { UserAppMemberServiceProxy umsvc = new UserAppMemberServiceProxy(); var um = umsvc。LoadEntityByKey (Cntx ApplicationContext.App。ID, ID); if (um == null) 返回错误; 嗯。IconImg = imgdata; 嗯。IsIconImgLoaded = true; 嗯。IsIconImgModified = true; 嗯。IconMime = mineType; 嗯。IconLastModified = lastModified; var result =等待umvc . addorupdateentitiesasync ( Cntx, 新UserAppMemberSet (), new UserAppMember[] {um}); 返回(result.ChangedEntities[0]。OpStatus,(int) EntityOpStatus.Updated)比;0; } 3.3.3成员的照片^照片数据包含一个成员的照片属性用户详细信息实体内部的UserDetails数据集。如何处理它的方式非常类似于前面讨论过的一个关于用户的图标图像数据中包含UserAppMembers数据集在这里不会重复。感兴趣的读者可以阅读相应的代码来了解更多的方法。3.4概要:成员属性^成员资格+系统的数据模式在第一部分介绍。这里的介绍提供了用于描述成员的数据集的补充细节。会员+系统中有四种关于会员的信息:关于会员的强制性公共信息。它保存在用户数据集中关于成员的记录中,包含成员的简要信息,如强制的用户名、登录密码的哈希值、可选的成员名和姓。强制性的特定于应用程序的成员简要信息。它保存在UserAppMembers数据集中关于该成员的记录中。用户可以根据所使用的web应用程序选择在记录中放置不同的(或不)成员信息。在这里,成员主电子邮件地址电子邮件是必需的属性,但用户图标图像IconImg是可选的。可选的特定于应用程序的关于成员的详细信息。它保存在UserDetails数据集中关于成员的记录中。它包含一个用户个人属性的暂定列表、一个用户描述和一个用户照片数据字段。根据应用程序场景,可以扩展、修改甚至缩小属性列表,而不需要做太多工作。成员类型的可选的特定于应用程序的“通信通道”列表。它被保存在一个记录中关于成员在通信数据集。所有种类的可寻址目的地,其他人可以使用到达成员,包括成员的hom/工作地址,电话号码,电子混乱老化的渠道,包括但不限于电子邮件地址等,包括在这里。数据模式的设计允许用户在同一类别中拥有多个通信通道。3.5账户信息^用户的账户信息与上述强制性记录中保存的属性子集有关,即类型1和类型2的数据集。下面显示了会员个人信息管理web页面的快照。图:用户帐号信息管理页面。它有两个标签页“个人信息”和“管理密码”。Bootstrap css类用于实现tabbing:复制代码<ul class="nav导航-tab "> & lt;李类=“活跃”比; a href="# Personal -info" data-toggle="tab" Personal Information< & lt; / li> & lt; li> a href="#password-panel" data-toggle="tab"管理密码</a> & lt; / li> & lt; / ul> & lt; div类=“tab-content”比; div id="personal-info" class="tab-pane active"> …个人信息部分… & lt; / div> & lt; div id =“密码面板”class =“标签面板”比; …密码管理部分… & lt; / div> & lt; / div> 3.5.1个人信息面板^左边“个人信息”部分是一个形式,其中包含用户的会员相关的属性,可以发回ChangeAccountInfo AccountController的方法,构建一个数据传输对象和调用ChangeAccountInfo MembershipContext类的方法,改变属性的更新。隐藏,副本Codepublic静态异步任务ChangeAccountInfo(字符串id, ApplicationUser user) { var cntx = cntx; UserServiceProxy usvc = new UserServiceProxy(); var u =等待usvc。LoadEntityByKeyAsync (cntx、身份证); if (u == null) 返回; u。FirstName = user.FirstName; u。LastName = user.LastName; 如果(u。IsFirstNameModified | | u.IsLastNameModified) 等待usvc。AddOrUpdateEntitiesAsync(cntx, new UserSet(), new User[] {u}); 如果(! string.IsNullOrEmpty (user.Email)) { UserAppMemberServiceProxy = new UserAppMemberServiceProxy(); var mb =等待mbsvc。LoadEntityByKeyAsync (cntx ApplicationContext.App。ID, ID); if (mb != null) { mb.Email = user.Email; 如果(mb.IsEmailModified) 等待mbsvc。AddOrUpdateEntitiesAsync (cntx新UserAppMemberSet (), new UserAppMember[] {mb}); } } } 考虑两个潜在涉及的数据集,即用户和UserAppMembers,因为成员的Email属性是特定于应用程序的。可以看到,它不允许用户将电子邮件地址的现有值重置为empty或null。这里为了避免在没有实际更新时调用后端服务,首先检查一个可编辑属性的“Is< propertyname>Modified”的伴随属性,其中“< propertyname>”是相应属性的名称。每当分配之前持久化的实体的相应属性时,这个companying属性就会自动更新。该方法仅在检测到任何更改时才执行实际更新。3.5.2密码管理面板^这是标准的密码更新面板。密码更新信息包含在相应的表单发布到ChangePassword AccountController类的方法,而不是处理密码更新应用程序的内部逻辑层(即MembershipPlusAppLayer45项目中定义的),它使用一个Asp。Net用户存储在这里讨论通过UserManagerEx类实现的API:UserManager.ChangePasswordAsync(user . identifier . getuserid (), model.OldPassword, model.Password); 这是因为更改密码是一种与安全性相关的操作,最好由一个更专门于安全性的模块来处理,即我们的Asp的自定义实现。Net身份管理系统。3.6会员详细资料^用户的详细资料是由上述可选记录中保存的属性和数据的子集组成,即用户详细资料数据集。会员无须建立会员详细资料档案。UserDetails有两种输出。cshtml,取决于用户是否有详细记录:Hide 复制Code@if(模型。详细信息= = null) { …当没有细节记录时输出内容… } 其他的 { …当详细信息记录存在时输出内容… } 当没有详细信息记录时,页面包含两个选项卡页详细信息创建面板,允许成员创建一个空的详细教授ile。成员通信通道管理面板,将在下一节中进行描述。否则,在成员属性和phto更新面板中,成员可以更新其个人属性。还有一个超链接指向成员的phto更新页面。成员描述更新面板。在这里,用户可以使用html编辑器编写关于自己的详细信息。成员通信通道管理面板,将在下一节中进行描述。两者的共同面板,即成员通信信道管理面板将在下一节中单独描述。这是因为使用了使用jQuery和KnockoutJS的客户端技术来处理它,这需要更详细的介绍。^ AccountController中的UserDetails操作非常简单,因为它将加载委托给MembershipContext类的GetUserDetails方法: 隐藏,收缩,复制Codepublic静态异步任务GetUserDetails(字符串id, bool direct = false) { // //从UserDetails数据集加载数据记录(如果有的话) // udsvc = new UserDetailServiceProxy(); var cntx = cntx; cntx。DirectDataAccess =直接; var details =等待udsvc。LoadEntityByKeyAsync (cntx ApplicationContext.App。ID, ID); UserDetailsVM m = null; if (details != null) { 如果(! details.IsDescriptionLoaded) { 细节。描述= udsvc.LoadEntityDescription (cntx ApplicationContext.App。ID, ID); 细节。IsDescriptionLoaded = true; } m = new UserDetailsVM {Details = Details}; m。IsPhotoAvailable = ! string.IsNullOrEmpty (details.PhotoMime); } 其他的 { m = new UserDetailsVM(); m。IsPhotoAvailable = false; } UserDetailSet uds = new UserDetailSet(); m。性别= uds.GenderValues; // //加载用户通信通道类型 // qexpr = new QueryExpresion(); qexpr。OrderTks = new List<QToken>(new QToken[] { new QToken {TkName = "ID"}, new QToken {TkName = "asc"} }); CommunicationTypeServiceProxy ctsvc = new CommunicationTypeServiceProxy(); var lct =等待ctsvc。QueryDatabaseAsync(Cntx, new CommunicationTypeSet(), qexpr) m。ChannelTypes = lct.ToArray (); // //加载当前成员下的用户通信通道 / /应用程序上下文。 // 通信服务代理csvc = new CommunicationServiceProxy(); qexpr = new QueryExpresion(); qexpr。OrderTks = new List<QToken>(new QToken[] { new QToken {TkName = "TypeID"}, new QToken {TkName = "asc"} }); qexpr。FilterTks = new List< new QToken {TkName = "UserID"}, new QToken {TkName =" =="}, new QToken {TkName = "\"" + id + "\""}, new QToken {TkName = "&&"}, new QToken {TkName = "ApplicationID"}, new QToken {TkName =" =="}, new QToken {TkName = "\"" + ApplicationContext.App。ID + "\"} }); var lc =等待csvc。QueryDatabaseAsync(Cntx, new CommunicationSet(), qexpr) foreach (lc中var c) { c。评论=等待csvc。LoadEntityCommentAsync (Cntx c.ID); c。IsCommentLoaded = true; c。通信typeref =等待csvc。MaterializeCommunicationTypeRefAsync (Cntx c); m.Channels。添加(new {id = c.ID, 标签= c.DistinctString, addr = c.AddressInfo, 评论= c.Comment, 类型id = c.CommunicationTypeRef。TypeName}); } 返回m; } 这个方法构造了一个UserDetailsVM类的实例,并使用从三个数据集加载的数据更新它:UserDetails、CommunicationTypes和Communications,这些数据集将由UserDetails使用。cshtml web页面。^约束集的概念只适用于依赖于其他数据集的数据集。通过约束集,我们指的是所述数据集实体的子集,其中的一些外键值作为固定值提供。用户的通信通道是一组受限的通信通道,它们的外键ApplicationID和UserID是固定的,并且具有任意类型的id。上面的代码使用通用的查询API方法来获取这个子集:复制CodeCommunicationServiceProxy csvc = new CommunicationServiceProxy(); qexpr = new QueryExpresion(); qexpr。OrderTks = new List<QToken>(new QToken[] { new QToken {TkName = "TypeID"}, new QToken {TkName = "asc"} }); qexpr。FilterTks = new List< new QToken {TkName = "UserID"}, new QToken {TkName =" =="}, new QToken {TkName = "\"" + id + "\""}, new QToken {TkName =“,和”}, new QToken {TkName = "ApplicationID"}, new QToken {TkName =" =="}, new QToken {TkName = "\"" + ApplicationContext.App。ID + "\"} }); var lc =等待csvc。QueryDatabaseAsync(Cntx, new CommunicationSet(), qexpr) 对于这种类型的查询,有一个等价但更具体的API方法:Hide复制Codevar fkeys = new CommunicationSetConstraints { ApplicationIDWrap = new ForeignKeyData<string>{ KeyValue = ApplicationContext.App。ID}, TypeIDWrap = null, //对类型没有限制 UserIDWrap = new ForeignKeyData<string>{KeyValue = id} }; var lc =等待csvc。ConstraintQueryAsync(Cntx, new CommunicationSet(), fkeys, null); 更简洁,更有意义。3.6.2缺少详细资料^在会员创建详细资料前,用户详细资料页面如下图:缺少用户详细资料记录时的用户详细资料页面。点击“现在创建”按钮后,用户详细信息记录初始创建后就变成了图形:用户详细信息页面。创建是由AccountController类的CreateUserDetails方法处理的:Hide复制代码(HttpGet) (授权) 公共异步Task< ActionResult>CreateUserDetails () { 等待MembershipContext.CreateUserDetails (User.Identity.GetUserId ()); 返回RedirectToAction(“UserDetails”、“账户”); } 它调用MembershipContext类的CreateUserDetails方法:Hide复制代码公共静态异步任务CreateUserDetails (string id) { udsvc = new UserDetailServiceProxy(); var ud =等待udsvc。LoadEntityByKeyAsync (Cntx ApplicationContext.App。ID, ID); if (ud == null) { 等待udsvc。AddOrUpdateEntitiesAsync (Cntx新UserDetailSet (), 新UserDetail [] { 新UserDetail { 用户id = id, ApplicationID = ApplicationContext.App.ID, CreateDate = DateTime.UtcNow } }); } 返回true; } 3.6.3详细资料管理器^该网页包含两个表单,可以独立提交和一个链接到成员照片更新页面。3.6.3.1成员属性^用户详细信息管理页面的左侧包含一个以表单形式包装的成员属性列表及其当前值。图:用户详细信息页面。更新由AccountController类的UpdateUserProperties方法处理,该方法将更新委托给MembershipContext类的UpdateUserProperties方法。隐藏,收缩,复制Codepublic静态异步任务UpdateUserProperties (string id, UserDetailsVM模型) { udsvc = new UserDetailServiceProxy(); var cntx = cntx; var details =等待udsvc。LoadEntityByKeyAsync (cntx ApplicationContext.App。ID, ID); int chgcnt = 0; 如果细节。性别! = model.Gender) { 细节。性别= model.Gender; chgcnt + +; } 如果(! details.BirthDate。HasValue,,model.BirthDate。HasValue | | details.BirthDate。HasValue,,model.BirthDate !HasValue | | details.BirthDate。HasValue,,model.BirthDate。HasValue,, details.BirthDate。价值! = model.BirthDate.Value) { 细节。生日= model.BirthDate; chgcnt + +; } 如果(chgcnt比;0) { 细节。LastModified = DateTime.UtcNow; udsvc。AddOrUpdateEntities (Cntx新UserDetailSet (), new UserDetail[] {details}); } 返回await GetUserDetails(id, true); } 在这里,实体的相关属性的原始值再次根据更新后的值进行检查。只有当至少一个相关属性被修改时,更新才会被执行。该方法在返回时调用GetUserDetails,并将第二个参数设置为true。如上所示,第二个参数的值被传递给cntx。DirectDataAccess财产。属性的值用来控制缓存读取操作的行为在数据服务方面,如果该值被设置为true,然后,而不是返回一个旧的缓存副本,如果有的话,刚刚更新的值(s)将被检索并返回给浏览器,浏览器将显示更改,而不必等到缓存到期。3.6.3.2会员照片^用户点击图片下方的“更新照片”链接,即可进入会员照片更新页面。图:照片更新页面。本方法中图像的更新和显示方式将在章节3.3中讨论,这里不再赘述。成员描述^成员描述面板有两个标签页:描述编辑器是附加到ckeditor WYSIWYG html编辑器。post-viewer。post-viewer显示已经保存的html格式的成员描述。因此,如果一个人改变了描述,他/她不应该期望看到改变之前立即寄回来。但是,编辑器的所见即所得特性使预览成为不必要的。图:成员描述编辑器。该描述被post回AccountController类Hide的UpdateUserDescription方法。复制代码(HttpPost) (授权) [ValidateInput (false)] [OutputCache(NoStore = true, Duration = 0)] 公共异步Task< actionresult>UpdateUserDescription (UserDetailsVM米) { //定制验证器… m = await MembershipContext.UpdateUserDescription( User.Identity.GetUserId (), m); 返回视图(“UserDetails”,m); } & lt; / actionresult> 属性[ValidateInput(false)]禁止正常内容验证,因为返回的内容是以HTML片段的形式,这将被普通内容验证器阻止。这里剩下的改进是引入一个定制的内容验证器,它可以用来过滤返回的内容,从而使其符合组织的安全标准。它调用MembershipContext类的UpdateUserDescription方法。该方法的实现与上面描述的方法非常相似。这里不再重复。3.7用户通信通道^用户的通信通道与存储在上述列表记录中的通信相关数据结构的一个子集相关,即通信数据集,一个成员可以记录0到他认为合适的任意数量的通信通道。成员的通信通道列表的处理方式与目前使用的更传统的服务器端技术有所不同。这里使用了jQuery支持的基于JavaScript的客户端方法KnockoutJS。KnockoutJS支持MVVM架构模式,该模式解耦了视图和代码之间的紧密绑定,使两者在不同的应用程序上下文中可分别重用。例如,后端数据服务提供的许多KnockoutJS视图模型可以在应用程序层中使用,只需稍加修改。它可以节省开发人员创建、维护和保持数据模式更改同步所需的大量时间。Asp。Net Mvc已经内置了对映射两个世界所需的大部分支持:强类型的。Net数据结构和用JSON表示的数据结构。本节还为在后续开发中更广泛地使用该技术做了准备。UserDetails。cshtml web页面有一个脚本部分,它将被注入到浏览器加载的输出html页面中:收缩,复制Code@section脚本{ … @Scripts.Render(~ /包/击倒) & lt;脚本src = " .content(~ /脚本/ DataService / UserDetailsPage.js)”在 & lt; / script> … & lt; script> appRoot = .content (~ /); $(函数(){ var vm = new UserDetails(); @foreach (var m in Model.ChannelTypes) { & lt; text> vm.channelTypes。push(新ChannelType (@m。ID、“@m.TypeName”));& lt; / text> } @ if(模型。细节! = null) { foreach (var c in Model.Channels) { & lt;一个名称=“member-channel-push声明”祝辞& lt; text> & lt; / a> vm.Channels.push ( 新频道( JSON.parse ( “@Html.Raw ( Utils.GetDynamicJson (c) )” ) ) ); & lt; / text> } } … ko。applyBindings (vm, $(“#通道”)[0]); }); & lt; / script> } 首先,我将提供一个概述,在没有定义所有内容的情况下为读者提供整个流程的概况,这是下面几个子部分的工作。除了视图\Schared\_Layout中定义的默认值之外。cshtml,它在web应用程序的Scripts\DataService子目录下加载KnockoutJS JavaScript包和UserDetailsPage.js。它在客户端加载页面后所做的是创建一个KnockoutJS视图模型对象var vm = new UserDetails();然后在servier端生成的JavaScript语句列表中将相应的元素推送到channelTypes和Channels中,也就是@foreach (Model.ChannelTypes中的var m){…}和@foreach (var c in Model.Channels){…}服务器端语句。在构建完整的对象vm之后,通过KnockoutJS方法ko将其绑定到web页面中id = "channels"的页面内的html元素。$ (' # applyBindings (vm通道”)[0]);在底部。上述html元素是在部分视图_PersonalChannelsPartial中定义的。视图\帐户子目录下的cshtml,该子目录由UserDetails.cshtml包含。隐藏,收缩,复制Code@model Archymeta.Web.MembershipPlus.AppLayer.Models.UserDetailsVM @using Microsoft.AspNet.Identity; @using Archymeta.Web.MembershipPlus.AppLayer; @using Archymeta.Web.Security.Resources; <table id="channels" class="table personal-channels"> & lt; thead> & lt; tr> & lt; th> @ResourceUtils。GetString (" a1fa27779242b4902f7ae3bdd5c6d509”、“类型”) & lt; / th> & lt; th> @ResourceUtils。GetString (" dd7bf230fde8d4836917806aff6a6b28”、“地址”) & lt; / th> & lt; th> @ResourceUtils。GetString (" 0 be8406951cdfda82f00f79328cf4efd”、“评论”) & lt; / th> & lt; th colspan =“2”风格= "宽度:1 px;空白:nowrap;}”在 @ResourceUtils。GetString(“456 d0deba6a86c9585590550c797502e”、“业务”) & lt; / th> & lt; / tr> & lt; / thead> <a name="knockout_channels_bind"><tbody data-bind="foreach: Channels"> > & lt; tr> & lt; td> & lt;跨数据绑定= "文本:selectedType”祝辞& lt; / span> & lt; / td> & lt; td> & lt; div> <input data bind="value: addr" class="form-control" /> & lt; / div> & lt; / td> & lt; td> & lt; div> <input data-bind="value: comment" class="form-control" /> & lt; / div> & lt; / td> & lt; td风格= "宽度:1 px; text-align:中心;“比; & lt; !—ko if: isModified—> 按钮class="btn btn-default btn-xs" 点击:function(data, event) { _updateChannel(数据、事件)}” title =“…”在 & lt;跨类= " ion-arrow-up-a "祝辞& lt; / span> & lt; / button> & lt; !——/ ko祝辞 & lt; !—ko ifnot: isModified—> ,, & lt; !——/ ko祝辞 & lt; / td> & lt; td风格= "宽度:1 px; text-align:中心;“比; 按钮class="btn btn-default btn-xs" 点击:function(data, event) { _deleteChannel($parent, data, event)}" title =“…”在 & lt;跨类=“ion-close”=“颜色:红”祝辞& lt; / span> & lt; / button> & lt; / td> & lt; / tr> & lt; / tbody> & lt; tfoot> & lt; tr> & lt; td> & lt;选择类=“表单控件” 风格= " min-width: 100 px; " 数据绑定= "选项:channelTypes、 optionsValue:“id”, optionsText:“名字”, 价值:selectedType, optionsCaption:‘…’”比; & lt; / select> & lt; / td> & lt; td> & lt; div> 输入数据绑定= "值:newChannel & lt;。addr表单控件“类= /比; & lt; / div> & lt; / td> & lt; td> & lt; div> <input data-bind="value: newChannel.comment" class="form-control" /> & lt; / div> & lt; / td> & lt; td风格= "宽度:1 px; text-align:中心;“比; 按钮class="btn btn-default btn-xs" 点击:function(data, event) { _addNewChannel(数据、事件 “请选择类型”, '请输入地址')}" title="添加新频道…"> & lt;跨类= " ion-plus "祝辞& lt; / span> & lt; / button> & lt; / td> & lt; td风格= "宽度:1 px; text-align:中心;“比;& lt; / td> & lt; / tr> & lt; / tfoot> & lt; / table> 它负责为当前成员提供可编辑的当前通信通道列表。这里& lt; tbody>元素绑定到通道observableArray vm以创建通道列表。& lt; tfoot>元素用于为要添加到列表中的新元素提供模板,其中类型的选项绑定到vm的channelTypes observableArray上,成员属性绑定到vm的newChannel JSON属性上。下图是结果:图:会员沟通渠道编辑器。在底部的新元素占位符的右手边有一个add按钮。一个删除e按钮在每个列出的频道上,当列出的频道被修改时,一个更新频道将出现,正如它在上面第一个频道的右手边所显示的那样。KnockoutJS使得这种动态行为的实现非常简单和轻量级(也就是说,它不必涉及到与服务器的请求/响应的往返通信)。下面是对应于通信类型和通信数据集的两个KnockoutJS视图模型。隐藏,ChannelType(id, name) { var自我=; 自我。id = id; self.name =名称; } 函数通道(数据){ var自我=; 自我。data =数据; 自我。addr = ko.observable (data.addr); self.comment = ko.observable (data.comment); 自我。selectedType = ko.observable (data.typeId); 自我。isModified = ko.computed(function () { 返回self。addr() != self。data。addr | | self.comment () ! = self.data.comment; }); } 这些是简化的视图模型,其中只定义了当前感兴趣的一些数据属性。任何数据集的完全映射和同步的KnockoutJS视图模型都可以在成员关系+数据服务的Scripts\DbViewModels\MembershipPlus子目录下找到。这里只有可编辑属性与KnockoutJS observable相关。通道视图模型的构造函数接受一个JSON对象,该对象用于相应地初始化其成员。它有一个计算属性isModified,它可以感知被监视属性(addr和可观察的注释)中的任何更改,并将更改通知绑定到它的视图部分。例如,网页已经隐藏。Code<复印件;!—ko if: isModified—> 按钮class="btn btn-default btn-xs" 点击:function(data, event) { _updateChannel(数据、事件)}” title =“…”在 & lt;跨类= " ion-arrow-up-a "祝辞& lt; / span> & lt; / button> & lt; !——/ ko祝辞 & lt; !—ko ifnot: isModified—> ,, & lt; !——/ ko祝辞 这将根据被监视的属性是否更改为从服务器加载的相应属性来隐藏/显示更新按钮。当前情况的根视图模型是Hide复制代码函数UserDetails() { var自我=; 自我。channelTypes = []; 自我。渠道= ko.observableArray ([]); 自我。newChannel = { 类型id: 0, addr: ko.observable(”), 备注:ko.observable (") }; 自我。selectedType = ko.observable (); } 它包含了channelTypes中可能的通道类型的列表,channel中成员通信通道的可观察数组,newChannel中新通信通道的数据占位符,以及selectedType中当前通道类型。频道被绑定到频道列表元素(见这里),以便对于可观察阵列中的每个通道,它将生成相应的tr>tbody>下定义的子树元素。当在JavaScript代码中改变通道中的对象时,UI将自动更新。如上所示,成员通信编辑面板需要将通道和channelTypes初始化为当前成员状态,才能进行操作更改。至少有两种方法可以初始化它们。首先让页面在客户端加载,然后使用Ajax调用某个API方法来检索这两个列表。在服务器端生成列表。这两个列表都是客户端解释的JavaScript列表,服务器端不能直接对它们进行操作。不过,服务器端软件可以充当JavaScript代码生成器,生成浏览器解释的JavaScript语句,以便间接完成任务。这里采用第二种方法。因此,对于channelTypes属性,我们使用Hide。Copy Code@foreach (var m in Model.ChannelTypes) { & lt; text> vm.channelTypes。push(新ChannelType (@m。ID、“@m.TypeName”));& lt; / text> } 它能产生兽皮Codevm.channelTypes副本。推动(新ChannelType (1 ' HomeAddress ')); vm.channelTypes。推动(新ChannelType (2, ' WorkAddress ')); vm.channelTypes。推动(新ChannelType (3 ' DaytimePhone ')); … … 客户端的JavaScript语句序列。与Channel属性类似,但稍微复杂一点,因为Channel视图模型更复杂,它只接受结构化JSON数据作为输入到构造函数,也就是我们期望的隐藏。Codevm.Channels副本。推动(新频道(channel1); vm.Channels。推动(新频道(channel2); … … JSON对象channel1、channel1等是在客户端根据在服务器上生成的JSON对象的字符串表示来构造的,例如Hide,复制Codevar channel1 = JSON.parse('{ “id”:“0 c58b64e dd57 - 4889 a6da - 63 ac4a4f5308”, “标签”:“sysadmin夜间电话:1223-3221”, “addr”:“1223 - 3221”, “评论”:“”, “类型id”:“NighttimePhone” } ')) var channel2 =… … 这里,JSON对象的字符串形式是在服务器上生成的。服务器实体^ UsersDetails调用MembershipContext的GetUserDetails方法。在服务器端加载时,cshtml使用与上面预期的JSON数据结构隐藏匹配的动态类型的对象填充Channels属性(UserDetailsVM!)复制Codeforeach(在lc中var c) { c。评论=等待csvc。LoadEntityCommentAsync (Cntx c.ID); c。IsCommentLoaded = true; c。通信typeref =等待csvc。MaterializeCommunicationTypeRefAsync (Cntx c); m.Channels。添加(new {id = c.ID, 标签= c.DistinctString, addr = c.AddressInfo, 评论= c.Comment, 类型id = c.CommunicationTypeRef。TypeName}); } 使用System.Web.Script中的JavaScriptSerializer,可以轻松地将动态类型的对象序列化为字符串形式。序列化名称空间。生成JavaScript部分(见这里)的JSON字符串隐藏复制代码“@Html.Raw (Utils.GetDynamicJson (c))” 的UserDetails。cshtml web页面调用在MembershipPlusAppLayer45项目中定义的Utils类的GetDynamicJson方法复制Codepublic静态字符串GetDynamicJson(对象obj) { JavaScriptSerializer ser = new JavaScriptSerializer(); 返回ser.Serialize (obj); } 用于连接服务器端。net对象和客户端JSON对象。3.7.4频道更新^ 3.7.4.1添加^添加按钮的点击事件,它只出现在底部的新频道模板的右边,是由以下JavaScript方法Hide处理的收缩,复制Codefunction _addNewChannel(data, event, typeMsg, addrMsg) { var typeId = data.selectedType(); var addr = data.newChannel.addr(); 如果(类型id) { if (addr != ") { . ajax({美元 url: appRoot +“Account/AddChannel”, 类型:“文章”, 数据类型:“json”, contentType:“application / json;charset = utf - 8”, 数据:JSON。stringify({类型id:类型id, 地址:addr,评论: data.newChannel.comment ()}), 成功:函数(内容){ 如果(! content.ok) { 警报(content.msg); 其他}{ data.selectedType(空); data.newChannel.comment ("); data.newChannel.addr ("); data.Channels。推动(新通道(content.data)); } }, 函数(jqxhr, textStatus) { 警报(jqxhr.responseText); }, 完成:函数(){ } }); 其他}{ 警报(addrMsg); } 其他}{ 警报(typeMsg); } } 它调用AccountController类Hide的AddChannel方法。复制代码(HttpPost) (授权) [OutputCache(NoStore = true, Duration = 0)] 公共异步Task< ActionResult>AddChannel(int typeId,字符串地址,字符串注释) { var data = await MembershipContext.AddChannel(User.Identity.GetUserId(), 类型id、地址、评论); 返回JSON(数据); } 它以JSON格式的数据结构返回添加的通道数据。被调用的MembershipContext类的AddChannel方法返回一个动态类型的Hide对象。收缩,复制Codepublic静态异步任务AddChannel(字符串id, int类型id,字符串地址,字符串注释) { 通信服务代理csvc = new CommunicationServiceProxy(); c = new Communication(); c。.ToString ID = Guid.NewGuid () (); c。类型id =类型id; c。用户id = id; c。ApplicationID = ApplicationContext.App.ID; c。AddressInfo =地址; c。评论=评论; var result =等待csvc。AddOrUpdateEntitiesAsync (Cntx新CommunicationSet (), 新通信[]{c}); 如果(result.ChangedEntities[0]。OpStatus,(int) EntityOpStatus.Added)比;0) { c = result.ChangedEntities [0] .UpdatedItem; c。通信typeref =等待csvc。MaterializeCommunicationTypeRefAsync (Cntx c); var dc = new { id = c.ID, 标签= c.DistinctString, addr = c.AddressInfo, 评论= c.Comment, 类型id = c.CommunicationTypeRef.TypeName }; 返回new {ok = true, msg = "", data = dc}; } 其他的 返回新{ 好吧= false, 味精= ResourceUtils。GetString (" 954122 aa46fdc842a03ed8b89acdd125”、“添加失败!”) }; } 返回的动态对象有一个国旗(ok)属性表明操作是否成功,一个味精属性,在失败的情况下提供了更多信息,和数据属性相对应的动态(类型)数据新添加的通信通道,以防操作成功。当操作符返回时,将执行以下代码Copy Codeif (!content.ok) { 警报(content.msg); 其他}{ data.selectedType(空); data.newChannel.comment ("); data.newChannel.addr ("); data.Channels。推动(新通道(content.data)); } 也就是说,如果不成功,它将显示错误消息,否则,它将首先清除newChannel对象,然后添加一个从返回的JSON数据构造的新channel视图模型对象。KnockoutJS框架将确保添加的通道将自动添加到视图(浏览器内的web页面)中。3.7.4.2修改^现有频道右侧的更新按钮只有在该频道被修改时才会出现。它的单击事件由以下JavaScript方法Hide处理收缩,Copy Codefunction _updateChannel(数据,事件){ . ajax({美元 url: appRoot +“Account/UpdateChannel”, 类型:“文章”, 数据类型:“json”, contentType:“application / json;charset = utf - 8”, 数据:JSON。stringify ({id: data.data.id, 地址:data.addr (), 备注:data.comment ()}), 成功:函数(内容){ 如果(! content.ok) { 警报(content.msg); 其他}{ data.data。addr = data.addr (); data.addr ("); data.addr (data.data.addr); data.data.comment = data.comment (); data.comment ("); data.comment (data.data.comment); } }, 函数(jqxhr, textStatus) { 警报(jqxhr.responseText); }, 完成:函数(){ } }); } 它调用AccountController类的UpdateChannel方法,该方法反过来调用MembershipContext类中的相应方法,我们不应该详细描述该方法,因为它与上述情况非常相似。3.7.4.3删除^删除按钮位于已存在通道的右侧。它的单击事件由以下JavaScript方法Hide处理收缩,复制Codefunction _deleteChannel(parent, data, event) { . ajax({美元 url: appRoot +“Account/DeleteChannel”, 类型:“文章”, 数据类型:“json”, contentType:“application / json;charset = utf - 8”, 数据:JSON。stringify ({id: data.data。id}), 成功:函数(内容){ 如果(! content.ok) { 警报(content.msg); 其他}{ var cnt = parent.Channels().length; for (var i = 0;我& lt;问;我+ +){ var c = parent.Channels()[i]; 如果(c.data。id == data.data.id) { parent.Channels.remove (c); 打破; } } } }, 函数(jqxhr, textStatus) { 警报(jqxhr.responseText); }, 完成:函数(){ } }); } 它调用AccountController类的DeleteChannel方法,该方法反过来调用MembershipContext类中的相应方法,我们不应该详细描述该方法,因为它与“add”的情况非常相似。这就完成了本文范围的实现细节。虽然冗长的描述可能会给读者留下这样的印象:即使是目前非常简单的功能,要使其工作起来也是相当复杂的,但一旦他/她习惯了可以应用于更复杂情况的工作流和模式,就不那么复杂了。尽管基于JSON的数据和JavaScript编码框架非常灵活和便宜,但它们并不容易维护。简单的键入错误或更改后的数据模式的错误同步(开发人员记住或写在设计文件上的错误)可能导致潜在的未来运行时错误,而不是即时编译时错误,而没有建立一个广泛的测试计划和文档。因此,这些松散类型系统的维护者或开发人员从他们的前辈那里继承了更高的“技术债务”。因此,一些强类型后端和JavaScript的平衡混合使用基于前端似乎是保持可维护性和灵活性的好方法。但是,当它们被用于在由强类型系统支持的薄代理层或适应层中连接异构系统时,它们可能非常有用,因为它们允许所谓的鸭子类型,这可以显著简化映射过程。4. 在<system.serviceModel>中,默认的数据服务根url设置为http://localhost/membp网络的节点。web应用程序的配置文件。阅读器可以将其更改为指向他/她设置数据服务的位置,或者在本地机器上使用web应用程序名称(即membp)设置数据服务。5. 历史^ 2014-02-25。文章版本1.0.0,初始版本。2014-03-02。文章版本1.0.5,UserDetails数据集的数据模式被更改。数据服务的改进文档。对源代码进行了修改。如果读者对Git源码控制系统有足够的了解,可以在github.com上查看Git库。本文的源代码是在codeproject-2分支上维护的,也就是这里。 本文转载于:http://www.diyabc.com/frontweb/news17328.html