65.9K
CodeProject 正在变化。 阅读更多。
Home

CodeStash - 一款对开发者有用的(希望如此)工具 II

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (39投票s)

2012年3月21日

CPOL

25分钟阅读

viewsIcon

133629

分布式代码片段存储工具:第二部分

CodeStash 文章列表

目录

引言

上次我们讨论了 CodeStash 的实际作用,以及网站的一些高级方面,并展示了一些屏幕截图。这次我们将深入探讨构成 CodeStash 网站的每个页面。所以让我们继续查看这些页面,所有这些都将在下面讨论。

低级架构

在浏览 CodeStash 网站的各个页面时,我将采用的通用方法是每次都遵循相同的模式,该模式如下:

  1. 我将显示给定页面的屏幕截图(如我们在第一篇文章中看到的)
  2. 然后我将讨论该页面如何工作以及它试图提供什么功能
  3. 然后我将展示控制器最相关的部分
  4. 然后我将展示 CsHtml 文件最相关的部分
  5. 然后我将展示 JavaScript 文件最相关的部分

登录

CodeStash 背后的想法是提供一个灵活的登录模型,允许用户注册或使用一些现有的 OpenId 提供商(例如 Google/AOL 等)登录。

我还希望 OpenId 登录与标准 ASP .NET 表单身份验证机制协同工作。关于如何做到这一点的信息真的不多,特别是如果 Web 不是您的主要技能(就像我一样)。

有一个出色的入门项目,介绍如何将 OpenId 和标准 ASP .NET 表单身份验证集成到 ASP MVC 3 应用程序中,这被用作 CodeStash 的基础,可在此处获取:
http://weblogs.asp.net/haithamkhedre/archive/2011/03/13/openid-authentication-with-asp-net-mvc3-dotnetopenauth-and-openid-selector.aspx

为了执行 OpenId 身份验证,我使用了出色的免费 .NET 库“DotNetOpenAuth.dll”。

我对此进行了大量扩展,您现在在 CodeStash 中看到的功能如下:

任何需要 OpenId 授权的标准 ASP .NET MVC 控制器操作都将简单地使用标准 [Authorize] 属性,这足以使其与此 OpenId/表单身份验证代码链接。

这个入门项目提供了以下功能:

  1. 使用 OpenId 登录
  2. 注册为新用户(不使用现有 OpenId 登录)

登录过程的第一步是用户登录到他们的 OpenId 提供商,完成后,用户将获得由他们的提供商提供的 OpenId 令牌。下一步是将用户信息与获得的 OpenId 令牌关联起来,并将其存储为标准的 ASP .NET Membership 用户。此时,新创建的 ASP .NET Membership 用户的详细信息也用于创建身份验证 cookie,该 cookie 存储用于表单身份验证机制,然后用户被视为已授权,并将被允许访问 CodeStash 网站的其余部分。

当用户被 OpenId 提供商视为已授权时,将创建一个新的 ASP .NET Membership 用户,以及一个加密的 CodeStash 令牌,理想情况下该令牌将通过电子邮件发送给用户,但尚未完成。因此,折衷方案是,我们向已认证的用户显示一个标准的 ASP .NET MVC 视图,其中显示加密的 CodeStash 令牌,并要求他们记下它,以便输入到 VS2010 插件宿主应用程序中,如 Pete 即将发布的文章中所述。

一旦完成首次登录并创建了 ASP .NET Membership 用户,用户后续登录只需点击其 OpenId 提供商按钮,即可获取 OpenId 令牌,用户只需简单点击一下即可登录。

我使用的 OpenId 库“DotNetOpenAuth.dll”也可以通过自定义配置部分进行配置,如下所示。

<configSections>
  <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true" />
</configSections>

<!--OpenId settings-->
<dotNetOpenAuth>
  <openid>
    <relyingParty>
      <security requireSsl="false" />
      <behaviors>
        <!-- The following OPTIONAL behavior allows RPs to use SREG only, but be compatible  
                                with OPs that use Attribute Exchange (in various formats). -->
        <add type="DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform, DotNetOpenAuth" />
      </behaviors>
    </relyingParty>
  </openid>
  <messaging>
    <untrustedWebRequest>
      <whitelistHosts>
        <!-- since this is a sample, and will often be used with localhost -->
        <add name="localhost" />
      </whitelistHosts>
    </untrustedWebRequest>
  </messaging>
  <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
  <reporting enabled="true" />
</dotNetOpenAuth>

所以这就是它背后的总体思想,你准备好深入一点了吗?来吧,这会很有趣。

登录过程在 AccountController 中进行,工作原理如下:

  1. 请求需要授权的页面(使用标准 AuthorizeAttribute)。
  2. 如果用户未通过身份验证,则重定向到 GET AccountController Logon 操作。
  3. 显示登录视图,其中包含所有 OpenID 提供商链接,HTML 表单设置为 POST 到 AccountController POST Logon 操作。
  4. 用户选择一个 OpenID 提供商并点击它,这将调用 JavaScript,实际上只是导致 OpenID 提供商字符串被存储,并向 AccountController Logon 操作发出 POST 请求。
  5. AccountController 的 POST Logon 操作执行两件事
    1. 它添加了一个 ClaimRequest,其中它要求 OpenID 提供商在 OpenID 提供商成功响应中包含的数据中包含电子邮件/全名。
    2. 然后重定向 OpenID 提供商网站(通过 DotNetOpenAuth.dll 的魔力),用户在此处输入其详细信息。
  6. 如果用户在 OpenID 提供商的网站输入有效凭据,他们将被重定向到 AccountController 上的默认登录操作(通过 DotNetOpenAuth.dll 的魔力),此时,它将检查 OpenID 提供商 IAuthenticationResponse 响应的结果,该结果可通过调用 OpenIdRelyingParty 类型的 GetResponse() 方法获得。如果响应为 AuthenticationStatus.Authenticated,则用户被视为已验证,然后可以从 OpenID 提供商的响应中请求更多用户详细信息,这通过使用 response.GetUntrustedExtension<ClaimsResponse> / response.GetExtension<ClaimsResponse> 完成,其中 ClaimsResponse 是与我们在步骤 5 中要求 OpenID 提供商在 AccountController 的 POST 登录操作中包含的 ClaimsResponse 匹配的响应。然后我们可以使用 ClaimsResponse 从 OpenID 提供商的 ClaimsResponse 中获取用户的电子邮件和全名。

以下是 AccountController 的 OpenId 方法最相关的部分

using System;
using System.Web.Mvc;
using System.Web.Security;
using CodeStash.Models.Security;
using CodeStash.Services;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.RelyingParty;

namespace CodeStash.Controllers
{
    public class AccountController : Controller
    {
        private static OpenIdRelyingParty openid = new OpenIdRelyingParty();
        private IFormsAuthenticationService formsService;
        private IMembershipService membershipService;
        private ILoggerService loggerService;

        public AccountController(   IFormsAuthenticationService formsService, 
                                    IMembershipService membershipService,
                                    ILoggerService loggerService)
        {
            this.formsService = formsService;
            this.membershipService = membershipService;
            this.loggerService = loggerService;
        }


        public ActionResult LogOn()
        {
            loggerService.Info("LogOn GET");
            return View();
        }
        

        [HttpPost]
        [ValidateAntiForgeryToken(Salt = "LogOn")]
        public ActionResult LogOn(LogOnModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                if (membershipService.ValidateUser(model.UserName, model.Password))
                {
                    formsService.SignIn(model.UserName, model.RememberMe);
                    if (Url.IsLocalUrl(returnUrl))
                    {
                        loggerService.Info(string.Format("LogOn : Sucessful redirecting to {0}", returnUrl));
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        Session["EncryptedPasswordForUserToWriteDown"] = null;
                        loggerService.Error("LogOn : UnSucessful logon redirecting to Home");
                        return RedirectToAction("Index", "Home");
                    }
                }
                else
                {
                    loggerService.Error("LogOn : The user name or password provided is incorrect.");
                    ModelState.AddModelError("", "The user name or password provided is incorrect.");
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }



        [ValidateInput(false)]
        public ActionResult Authenticate(string returnUrl)
        {
            var response = openid.GetResponse();
            if (response == null)
            {
                //Let us submit the request to OpenID provider
                Identifier id;
                if (Identifier.TryParse(Request.Form["openid_identifier"], out id))
                {
                    try
                    {
                        var request = openid.CreateRequest(Request.Form["openid_identifier"]);


                        var claim = new ClaimsRequest
                        {
                            Email = DemandLevel.Require,
                            Nickname = DemandLevel.Require,
                            FullName = DemandLevel.Request,
                        };

                        var fetch = new FetchRequest();
                        fetch.Attributes.AddRequired(WellKnownAttributes.Name.First);
                        fetch.Attributes.AddRequired(WellKnownAttributes.Name.Last);

                        request.AddExtension(claim);
                        request.AddExtension(fetch);



                        return request.RedirectingResponse.AsActionResult();
                    }
                    catch (ProtocolException ex)
                    {
                        ViewBag.Message = ex.Message;
                        return View("LogOn");
                    }
                }

                ViewBag.Message = "Invalid identifier";
                return View("LogOn");
            }

            //Let us check the response
            switch (response.Status)
            {

                case AuthenticationStatus.Authenticated:
                    LogOnModel lm = new LogOnModel();
                    lm.OpenID = response.ClaimedIdentifier;

                    var claim = response.GetExtension<ClaimsResponse>();
                    var fetch = response.GetExtension<FetchResponse>();
                    var nick = response.FriendlyIdentifierForDisplay;
                    var email = string.Empty;

                    if (claim != null)
                    {
                        nick = string.IsNullOrEmpty(claim.Nickname) ? claim.FullName : claim.Nickname;
                        email = claim.Email;
                    }

                    if (string.IsNullOrEmpty(nick) && fetch != null &&
                        fetch.Attributes.Contains(WellKnownAttributes.Name.First) &&
                        fetch.Attributes.Contains(WellKnownAttributes.Name.Last))
                    {
                        nick = fetch.GetAttributeValue(WellKnownAttributes.Name.First) + " " +
                               fetch.GetAttributeValue(WellKnownAttributes.Name.Last);
                    }

                    MembershipUser user = membershipService.GetUserByOpenId(lm.OpenID);
                    Tuple<MembershipCreateStatus, String> resultsAndPassword = null;

                    if (user == null)
                    {
                        resultsAndPassword = membershipService.CreateUser(nick, email, lm.OpenID);
                        Session["EncryptedPasswordForUserToWriteDown"] = resultsAndPassword.Item2;
                        user = membershipService.GetUserByOpenId(lm.OpenID);
                    }
                    else
                    {
                        Session["EncryptedPasswordForUserToWriteDown"] = user.GetPassword();
                    }

                    //check if user is still empty, which means we have now managed to authenticate via OpenId
                    //and store in database
                    if (user != null)
                    {
                        lm.UserName = user.UserName;
                        formsService.SignIn(user.UserName, false);
                        Session["User"] = user;
                        
                        return RedirectToAction("Index", "Home");
                    }
                    else
                    {
                        return View("LogOn", lm);
                    }

                case AuthenticationStatus.Canceled:
                    ViewBag.Message = "Canceled at provider";
                    return View("LogOn");
                case AuthenticationStatus.Failed:
                    ViewBag.Message = response.Exception.Message;
                    return View("LogOn");
            }

            return new EmptyResult();
        }
    }
}

登录机制与标准 ASP MVC AuthorizeAttribute 动作过滤器协同工作,这可以在 CodeStash 中的任意数量的控制器动作中看到,示例如下:

[Authorize]
[RenderTagCloud]
public ActionResult About()
{
    return View();
}

正如我们之前讨论的,如果找不到表单身份验证令牌,并且控制器动作被标记为 [Authorize] 属性,则用户将被重定向到登录页面,直到他们登录。

这就是登录视图的样子,用户可以选择使用以下方式登录:

  • OpenId 身份验证,他们实际上不需要输入任何内容,只需点击他们的 OpenId 提供商(前提是他们之前已登录且表单身份验证 cookie 仍然存在)
  • 标准用户名/密码(来自注册过程)

有几点需要注意,例如:

  1. 浏览器的地址栏显示一个查询字符串,其中包含一个 ReturnUrl,它设置为 ReturnUrl=/Site/Data,这恰好是我们尝试加载的页面(需要授权才能查看)的控制器/操作 URL。这个 ReturnUrl 查询字符串参数是表单身份验证的一个标准功能,我们最终将用它来存储身份验证 cookie。
  2. 有相当多的图像按钮可以点击。这些图像中的每一个都代表一个您可以用来登录的 OpenID 兼容站点。例如,我有一个 Google 帐户,所以我可能会选择使用我的 Google 凭据。我应该指出,我从某个博客获得了 Logon.aspx 页面的大部分内容,但我记不起是从哪里来的,所以很抱歉本文没有直接提及来源。

如果我通过点击 Google 图片继续使用我的 Google 帐户,当前浏览器会话将导航到 Google,我可以在那里输入我的正常登录凭据,如下所示:

以下是登录视图标记最相关的部分

@model CodeStash.Models.Security.LogOnModel
@{    ViewBag.Title = "Log On";
}
@section SpecificPageHeadStuff
 {
    @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Account/accountFunctions.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.validate.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js"))
}
@using CodeStash.ExtensionsMethods
<h2>
    Log On</h2>
<p>
    You may login by choosing your OpenId Provider, or by entering your CodeStash username
    and password. Or you can @Html.ActionLink("Register", "Register")
    if you don't have an account.
</p>
<form action="Authenticate?ReturnUrl=@HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"])" 
        method="post" id="openid_form">
    <input type="hidden" name="action" value="verify" />
    <div class="logonBox">
        <fieldset>
            <legend>Login using OpenID</legend>
            <div class="openid_choice">
                <p>
                    Please click your account provider:</p>
                <div id="openid_btns">
                </div>
            </div>
            <div id="openid_input_area">
                @Html.TextBox("openid_identifier")
                <input type="submit" value="Log On" />
            </div>
            <noscript>
                <p>
                    OpenID is service that allows you to log-on to many different websites using a single
                    indentity. Find out <a href="http://openid.net/what/">more about OpenID</a> and
                    <a href="http://openid.net/get/">how to get an OpenID enabled account</a>.</p>
            </noscript>
            <div>
                @if (Model != null)
                {
                    if (String.IsNullOrEmpty(Model.UserName))
                    {
                    <div class="editor-label">
                        @Html.LabelFor(model => model.OpenID)
                    </div>
                    <div class="editor-field">
                        @Html.DisplayFor(model => model.OpenID)
                    </div>
                    <p class="button">
                        @Html.ActionLink("New User ,Register", "Register", new { OpenID = Model.OpenID })
                    </p>
                    }
                }
            </div>
        </fieldset>
    </div>
</form>
@Html.ValidationSummary(true, "Login was unsuccessful. Please correct the errors and try again.")
@using (Html.BeginForm("Logon", "Account", FormMethod.Post, new { id = "LogonForm" }))
{

    @Html.AntiForgeryToken("LogOn")
    
    <div class="logonBox">
        <fieldset>
            <legend>Or Login Normally</legend>
            <div class="editor-label">
                @Html.LabelFor(m => m.UserName)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.UserName, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.UserName)
            </div>
            <div class="editor-label">
                @Html.LabelFor(m => m.Password)
            </div>
            <div class="editor-field">
                @Html.PasswordFor(m => m.Password, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.Password)
            </div>
            <div class="editor-label">
                @Html.CheckBoxFor(m => m.RememberMe)
                <label for="RememberMe" class="centerAlignedText">
                    Remember me?</label>
            </div>
            <p>
                <br />
                <span class="btn"><a id="LogOnBtn" href="#">LogOn</a><span></span></span> <span class="clear">
                </span>
                <br />
            </p>
        </fieldset>
    </div>
}

登录视图标记中最重要的部分之一是实际的表单标签标记。登录视图实际上有两个独立的表单标签,它们处理不同类型的登录。

OpenId 登录表单标签

此表单确保调用 AccountController Authenticate 操作,我们已在上面的 AccountController 代码中看到。

<form action="Authenticate?ReturnUrl=@HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"])" 
        method="post" id="openid_form">
</form>

标准表单身份验证登录表单标签

此表单确保调用 AccountController Login POST 操作,我们已在上面的 AccountController 代码中看到。

@using (Html.BeginForm("Logon", "Account", FormMethod.Post, new { id = "LogonForm" }))
{

}

Register

这是您用于注册新用户的页面,该用户将使用标准表单身份验证,即:用户名/密码身份验证

由于表单认证在 ASP .NET 开发中非常有名,我不会用太多细节来烦您,我只会给您最基本的细节。

所以这就是 AccountController 的注册部分的样子

using System;
using System.Web.Mvc;
using System.Web.Security;
using CodeStash.Models.Security;
using CodeStash.Services;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.RelyingParty;

namespace CodeStash.Controllers
{
    public class AccountController : Controller
    {
        private static OpenIdRelyingParty openid = new OpenIdRelyingParty();
        private IFormsAuthenticationService formsService;
        private IMembershipService membershipService;
        private ILoggerService loggerService;

        public AccountController(   IFormsAuthenticationService formsService, 
                                    IMembershipService membershipService,
                                    ILoggerService loggerService)
        {
            this.formsService = formsService;
            this.membershipService = membershipService;
            this.loggerService = loggerService;
        }


        public ActionResult Register(string OpenID)
        {
            loggerService.Info("Register : New user registration selected");
            ViewBag.PasswordLength = membershipService.MinPasswordLength;
            ViewBag.OpenID = OpenID;
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken(Salt = "Register")]
        public ActionResult Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                // Attempt to register the user
                MembershipCreateStatus createStatus = membershipService.CreateUser(
			model.UserName, model.Password, model.Email, model.OpenID);

                if (createStatus == MembershipCreateStatus.Success)
                {
                    formsService.SignIn(model.UserName, false /* createPersistentCookie */);
                    loggerService.Info("Register : Sucess creating new user");
                    Session["EncryptedPasswordForUserToWriteDown"] = null;
                    return RedirectToAction("Index", "Home");
                }
                else
                {
                    ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));
                }
            }
            else
            {
                loggerService.Info("Register : There were some error registering, " +
			"possibly due to missing/incorrect registration settings being supplied");
            }

            // If we got this far, something failed, redisplay form
            ViewBag.PasswordLength = membershipService.MinPasswordLength;
            return View(model);
        }
 
    }
}

我们有以下注册视图标记:

@model CodeStash.Models.Security.RegisterModel

@{
    ViewBag.Title = "Register";
}
@section SpecificPageHeadStuff
 {
    @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Account/accountFunctions.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.validate.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js"))

}
@using CodeStash.ExtensionsMethods

<h2>Create a New Account</h2>
<p>
    Use the form below to create a new account. 
</p>
<p>
    Passwords are required to be a minimum of @ViewBag.PasswordLength characters in length.
</p>

@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { id = "RegisterForm" }))
{
    @Html.AntiForgeryToken("Register")
    @Html.ValidationSummary(true, 
        "Account creation was unsuccessful. Please correct the errors and try again.")
    <div>
        <fieldset>
            <legend>Account Information</legend>
            @if (ViewData["OpenID"] != null)
            {
            <div class="editor-label">
                @Html.Label("OpenID")
            </div>
            <div class="editor-label">
                @Html.Label((string)ViewBag.OpenID)
            </div>
            }
            <div class="editor-label">
                @Html.LabelFor(m => m.UserName)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.UserName, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.UserName)
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.Email)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.Email, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.Email)
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.Password)
            </div>
            <div class="editor-field">
                @Html.PasswordFor(m => m.Password, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.Password)
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.ConfirmPassword)
            </div>
            <div class="editor-field">
                @Html.PasswordFor(m => m.ConfirmPassword, new { style = "width:300px" })
                @Html.ValidationMessageFor(m => m.ConfirmPassword)
            </div>

            <p>
                <br />
                <span class="btn"><a id="RegisterBtn" 
			href="#">Register</a><span></span></span>
	            <span class="clear"></span>
			    <br />
            </p>

        </fieldset>
    </div>
}

老实说,这就是注册过程的全部内容,除了 Web.Config 的这一部分配置说明正在使用表单身份验证。

<authentication mode="Forms">
  <forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>

OpenId JavaScripts

OpenId 选择器还有一套 javascript,它基于一个免费提供的库,可从以下地址下载:http://code.google.com/p/openid-selector/

这将为您提供以下 4 个在主页面中注册的组件:

  • openid-en.js
  • openid-jquery.js
  • openid-shadow.css
  • openid.css

主页模板

主页(Views\Shared\_Layout.cshtml)提供了 CodeStash 网站使用的所有通用 CSS/Javascript。

如果您当前未登录,您将看到这样的主页

一旦您登录,您将看到这样的主页

下面显示的是主页的相关标记

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

@using Telerik.Web.Mvc.UI
@using CodeStash.ExtensionsMethods

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>@ViewBag.Title</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    @Html.CssTag(Url.Content("~/Content/openid-shadow.css"))
    @Html.CssTag(Url.Content("~/Content/openid.css"))
    @Html.CssTag(Url.Content("~/Content/themes/base/jquery-ui.css"))
    @Html.CssTag(Url.Content("~/Content/themes/base/jquery.ui.dialog.css"))
    @Html.CssTag(Url.Content("~/Content/Highlighting/jquery.snippet.css"))
    @Html.CssTag(Url.Content("~/Content/site.css"))


    @Html.ScriptTag(Url.Content("~/Scripts/jquery-1.6.4.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/modernizr-1.7.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.tools.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery.tmpl.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/openid-jquery.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/openid-en.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/jquery-ui-1.8.16.min.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/Common/commonFunctions.js"))
    @Html.ScriptTag(Url.Content("~/Scripts/Highlighting/jquery.snippet.js"))
   
  
    @( Html.Telerik().StyleSheetRegistrar().DefaultGroup(group => group
        .DefaultPath("~/Content/Telerik")
        .Add("telerik.common.css")
        .Add("telerik.Black.min.css"))
    )
    
    <script type="text/javascript">
        $(document).ready(function () {
            openid.init('openid_identifier');
        });
    </script>
    @RenderSection("SpecificPageHeadStuff", false)
</head>
<body>
    <div id="header">
    </div>
    <div id="main-wrapper">
        <img id="logo" src="../../Content/Images/Logo.png" />
        <img id="logoFiles" src="../../Content/Images/files.png" />
        <div id="main">
            <div id="sidebar">
                <div class="gadget">
                    <h2>Settings</h2>
                    <div class="clr">
                    </div>
                    <ul class="sb_menu">
                        <li>@Html.Partial("_LogOnPartial")</li>
                        <li><a href="https://codeproject.org.cn/Team">Team Settings</a></li>
                        <li><a href="https://codeproject.org.cn/Account/ChangePassword">Change Password</a></li>
                        <li><a href="https://codeproject.org.cn/Settings">Change Settings</a></li>
                    </ul>
                </div>
                <div class="gadget">
                    <h2>Actions</h2>
                    <div class="clr">
                    </div>
                    <ul class="sb_menu">
                        <li><a href="https://codeproject.org.cn/Search/CreateSearch">Search</a></li>
                        <li><a href="https://codeproject.org.cn/CodeSnippet">Add Code Snippet</a></li>
                        <li><a href="https://codeproject.org.cn/CodeSnippet/OpenFromWeb">Open From Web</a></li>
                    </ul>
                </div>
                @Html.Partial("_TagCloudPartial")
            </div>
            <div id="mainbar">
                @RenderBody()
            </div>
            <div class="clr">
            </div>
        </div>
    </div>
</body>
    @(Html.Telerik().ScriptRegistrar()
            .Globalization(true)
            .jQuery(false).DefaultGroup(group => group
                .DefaultPath("~/Scripts/Telerik")
                .Add("telerik.common.min.js")
                .Add("telerik.grid.min.js")
                .Add("telerik.textbox.min.js")
                .Add("telerik.calendar.min.js")
                .Add("telerik.datepicker.min.js")
                .Add("telerik.grid.filtering.min.js"))
    )
</html>

关于这个标记没什么可说的,除了它利用了我们上次看到的哈希 HtmlHelper 扩展方法来哈希 CSS/JavaScript 文件,它还注册了免费的 Telerik MVC 贡献控件,这些控件用于搜索,我们稍后会看到。

标签云

标签云是典型的超链接排列方式,其中云将所有已保存的代码片段分组到其相应的类别(数据库中的类别)中,并且仅将这些片段的最高排名总和显示为可在主页上使用的超链接集(前提是用户已登录)。

这就是标签云的样子。显然,下面显示的一些类别只是我为了说明问题而添加的虚拟类别。

标签云的渲染主要归功于一个名为 RenderTagCloudAttribute 的专用 ActionFilter,它只能用于全页视图。尽管没有任何东西可以阻止它用于不返回全页视图的控制器动作。

这是 RenderTagCloudAttribute 中的代码:

using System.Security.Principal;
using System.Web.Mvc;
using CodeStash.Controllers;

namespace CodeStash.Filters
{
    public class RenderTagCloudAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            IPrincipal user = ((Controller)filterContext.Controller).User;
      
            if (user != null && user.Identity.IsAuthenticated)
            {
                if (filterContext.Controller is BaseTagCloudEnabledController)
                {
                    ((BaseTagCloudEnabledController)filterContext.Controller).RenderAndCalculateTagCloud();
                }

                if (filterContext.Controller is BaseTagCloudEnabledAsyncController)
                {
                    ((BaseTagCloudEnabledAsyncController)filterContext.Controller).RenderAndCalculateTagCloud();
                }
            }
        }
    }
}

还有一个专门的 BaseTagCloudEnabledControllerBaseTagCloudEnabledAsyncController 可以继承,它们为标签云提供一些基本功能。这是相关的代码:

public abstract class BaseTagCloudEnabledAsyncController : AsyncController
{
    private readonly ITagCloudService tagCloudService;

    public BaseTagCloudEnabledAsyncController(ITagCloudService tagCloudService)
    {
        this.tagCloudService = tagCloudService;
    }


    public void RenderAndCalculateTagCloud()
    {
        Random rand = new Random();

        IEnumerable<TagCategoryModel> tags = this.tagCloudService.CreateTagCloud();
        if (tags.Any())
        {
            if (tags.Count() < 10)
                ViewData["TagCloud"] = tags;
            else
                ViewData["TagCloud"] = tags.Take(10);
        }
        else
            ViewData["TagCloud"] = new List<TagCategoryModel>();

        ViewData["Rand"] = rand;

    }
}

可以看出,这段代码使用了 tagCloudService,其工作原理如下:

using System.Collections.Generic;
using System.Linq;
using CodeStash.Common.DataAccess.EntityFramework;
using CodeStash.Common.DataAccess.Repository;
using CodeStash.Common.DataAccess.UnitOfWork;
using CodeStash.Models.TagCloud;

namespace CodeStash.Services
{
    public class TagCloudService : ITagCloudService
    {
        private readonly IUnitOfWork unitOfWork;
        private readonly IRepository<CodeSnippet> codeSnippetRepository;
        private readonly IRepository<CodeCategory> codeCategoryRepository;


        public TagCloudService(IUnitOfWork unitOfWork,
                              IRepository<CodeSnippet> codeSnippetRepository,
                              IRepository<CodeCategory> codeCategoryRepository)
        {
            this.unitOfWork = unitOfWork;
            this.codeSnippetRepository = codeSnippetRepository;
            this.codeCategoryRepository = codeCategoryRepository;
        }


        public IEnumerable<TagCategoryModel> CreateTagCloud()
        {

            using (unitOfWork)
            {
                codeSnippetRepository.EnrolInUnitOfWork(unitOfWork);
                codeCategoryRepository.EnrolInUnitOfWork(unitOfWork);

                int totalCodeSnippets = codeSnippetRepository.FindAll().Count();

                var categories = codeCategoryRepository.FindAll("CodeSnippets").AsEnumerable();
                var tagCategories = 
                    (from c in categories
                    orderby c.CodeCategoryName
                    select new TagCategoryModel
                    {
                        CategoryId = c.CodeCategoryId,
                        CategoryName = string.Format("{0}", c.CodeCategoryName.Trim()),
                        CountOfCategory = c.CodeSnippets.Count(),
                        TotalArticles = totalCodeSnippets
                    });

                return (from x in tagCategories
                        where x.CountOfCategory > 0
                        orderby x.CountOfCategory descending
                        select x).ToList();
            }
        }
    }
}

主页 _Layout.cshtml 中有这行 @Html.Partial("_TagCloudPartial"),它渲染了标签云局部视图,如下所示:

@if (ViewData["TagCloud"] != null)
{
    IEnumerable<CodeStash.Models.TagCloud.TagCategoryModel> cats =
        (IEnumerable<CodeStash.Models.TagCloud.TagCategoryModel>)ViewData["TagCloud"];

    if (cats.Any())
    {
        <div class="gadget">
            <h2>Tag Cloud</h2>
            <div class="clr">
            </div>
            <div id="tagCloud">
            
                @foreach (var t in cats)
                {
                    Random rand = (Random)ViewData["Rand"];
                    string[] colors = new string[] { "#21587D", "#3181B7", 
				"#1273B5", "#0D5382", "#2C5E7F", "#347096" };

                    <span>
                        @Html.ActionLink(string.Format("{0} ",t.CategoryName),
				"DisplaySnippetsForCategory","CodeSnippet",
                        new { category= t.CategoryName},
                        new 
                        { 
                            style=string.Format("color : {0}",colors[rand.Next(colors.Length)]),
                            @class = CodeStash.Utils.WebSiteUtils.GetTagClass(t.CountOfCategory, t.TotalArticles)
                        })
                    </span>
                }
            </div>
    </div>
    }
}

可以看出,这个视图简单地渲染了存储在 ViewData 中的数据。

个人资料设置

个人资料页面允许已登录的 CodeStash 网站用户调整其个人设置。目前仅限于 2 项设置,但将来可能会扩展。

  • 最大显示片段数:此设置将片段的显示限制为最大项目数。如果此数字超出,则应进行更好的搜索。
  • 片段高亮 CSS:这选择用于片段高亮的 CSS 样式。
public class SettingsController : BaseTagCloudEnabledController
{
    private readonly ILoggerService loggerService;
    private readonly IMembershipService membershipService;




    public SettingsController(
        ILoggerService loggerService, 
        IMembershipService membershipService,
        ITagCloudService tagCloudService)
        : base(tagCloudService)
    {
        this.loggerService = loggerService;
        this.membershipService = membershipService;
    }


    [Authorize]
    [RenderTagCloud]
    [HttpGet]
    public ActionResult Index()
    {

        if (User.Identity.IsAuthenticated)
        {
            MembershipUser user = 
		membershipService.GetUserByUserName(User.Identity.Name);
            UserSettingsProfileModel profile = 
		UserSettingsProfileModel.GetUserProfile(User.Identity.Name);
            ChangeSettingsModel vm = new ChangeSettingsModel();
            ....
            ....
            return View(vm);
        }
        else
        {
            return RedirectToAction("Index", "Home");
        }

    }


    [Authorize]
    [HttpPost]
    [AjaxOnly]
    [ValidateAntiForgeryToken(Salt = "ChangeSettings")]
    public ActionResult ChangeSettings(ChangeSettingsModel vm)
    {
        try
        {
            if (ModelState.IsValid)
            {
                MembershipUser user = 
			membershipService.GetUserByUserName(User.Identity.Name);
                UserSettingsProfileModel profile = 
			UserSettingsProfileModel.GetUserProfile(User.Identity.Name);
                ....
                ....
                profile.Save();
                ....
                ....
            }
            else
            {
                ViewData["successfulEdit"] = false;
                ....
                ....
            }
        }
        catch
        {
            ....
            ....
            ViewData["successfulEdit"] = false;
        }
    }
}

可以看出,SettingsController 只是简单地渲染了一个默认视图,并允许更新自定义的 UserSettingsProfileModel,如下所示:

public class UserSettingsProfileModel : ProfileBase
{
    [SettingsAllowAnonymous(false)]
    public bool IsOpenIdLoggedInUser
    {
        get { return (bool)base["IsOpenIdLoggedInUser"]; }
        set { base["IsOpenIdLoggedInUser"] = value; }
    }

    [SettingsAllowAnonymous(false)]
    public int HighlightingCSSId
    {
        get { return (int)base["HighlightingCSSId"]; }
        set { base["HighlightingCSSId"] = value; }
    }

    [SettingsAllowAnonymous(false)]
    public int MaxSnippetsToDisplay
    {
        get { return (int)base["MaxSnippetsToDisplay"]; }
        set { base["MaxSnippetsToDisplay"] = value; }
    }

    public static UserSettingsProfileModel GetUserProfile(string username)
    {
        return Create(username) as UserSettingsProfileModel;
    }

    public static UserSettingsProfileModel GetUserProfile()
    {
        return Create(Membership.GetUser().UserName) as UserSettingsProfileModel;
    }
}

SettingsController 代码中还可以看到它使用了 IMembershipService 类。这只是一个实现以下接口的帮助类,用于与标准 ASP .NET Membership 数据库进行通信。

public interface IMembershipService
{
    int MinPasswordLength { get; }
    bool ValidateUser(string userName, string password);
    MembershipCreateStatus CreateUser(string userName, string password, string email, string OpenID);
    Tuple<MembershipCreateStatus, String> CreateUser(string userName, string email, string OpenID);
    bool ChangePassword(string userName, string oldPassword, string newPassword);
    MembershipUser GetUserByOpenId(string OpenID);
    MembershipUser GetUserByUserName(string UserName);
}

好的,现在让我们继续查看页面标记,它非常简单,最重要的部分如下所示:

@model CodeStash.Models.Settings.ChangeSettingsModel
@{
	ViewBag.Title = "Change Settings";
	Layout = "~/Views/Shared/_Layout.cshtml";
}
@using CodeStash.ExtensionsMethods
@section SpecificPageHeadStuff
 {
     @Html.CssTag(Url.Content("~/Content/Controllers/Settings/settings.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Settings/settings.js"))
}


<div id="dialog-message"  style="display:none;">
	<p id="dialog-message-content">
	</p>
</div>

<div id="ChangeSettingsPanel">
    @Html.Partial("ChangeSettingsPartial",Model)
</div>

基本上,一个局部视图被渲染,显示了用户实际设置的所有标记。您可能可以从上面的设置屏幕截图中想象出这些标记。

那么,现在让我们看看 JavaScript 方面吧。设置 JavaScript 完整地显示在下面:

$(document).ready(function () {

    InitBinding();

});



function InitBinding() {

    console.log($('#successfulEdit').val());

	//On submit on add page, submit the add and shows the success
	//page it the edit was successful, otherwise add page is shown again including
	//validation errors
    $("#Submit").click(function (e) {
        e.preventDefault();
        CallPostRequestForChangeSetting();
    });
}



//Ajax request to load in next page of results
function CallPostRequestForChangeSetting() {

 
    var formData = $("#ChangeSettingsForm").serialize();

    $.post("/Settings/ChangeSettings", formData, function (response) {
        $("#AjaxSettingsContents").replaceWith(response);
        InitBinding();

        var successfulAdd = $('#successfulEdit').val();
        if (successfulAdd == "True") {
            showOkDialog('Sucessfully saved your settings', 180, 'Information');
        }
        else {
            showOkDialog('Could not update your user settings', 180, 'Error');
        }
    });
	return false;
}

 

团队设置

此页面允许登录用户创建团队。基本思想是,第一个创建团队的用户将成为团队所有者,他可以随后通过添加/删除团队成员来更改团队。

如果我们查看与团队相关的模式条目,应该不难看出这个屏幕是如何组合在一起的。

该页面基本上提供了这些步骤来允许创建新的用户团队。

创建新团队并为其命名

您只需输入新的团队名称并点击“创建团队”按钮即可创建一个全新的团队,然后您可以为其分配团队成员。或者,您可以选择您拥有的现有团队,这些团队显示在您可以在其中输入新团队名称的文本框正下方的选择中。

搜索要添加到团队的用户

一旦您创建了一个新团队或选择了一个要添加团队成员的现有团队,您必须搜索团队成员,这通过团队设置页面的以下部分完成。

  • 您必须输入团队成员的电子邮件
  • 您可以选择该团队成员的登录是否为 OpenId。这将有助于找到团队的精确用户。

所以它首先输入团队成员的电子邮件,然后点击“搜索用户”按钮,之后团队设置页面的更多部分将显示出来,显示与该电子邮件匹配的用户。这些用户显示在一个选择列表中,您可以从中选择他们并点击“将用户分配给团队”按钮。

操作团队成员

一旦您向选定的(或新的)团队添加了成员,您将在团队设置页面的底部区域看到他们全部显示。如果您决定要从团队中删除某个特定成员,您可以选择:

  • 点击该特定团队成员顶部的红色叉号
  • 将团队成员拖放到显示的垃圾桶中(如果您放手时不在垃圾桶上,它将飞回其旧位置)

这是团队页面标记最相关的部分

@model CodeStash.Models.Team.TeamModel
@{
	ViewBag.Title = "Team Settings";
	Layout = "~/Views/Shared/_Layout.cshtml";
}
@section SpecificPageHeadStuff
 {
	 @Html.CssTag(Url.Content("~/Content/Controllers/Team/team.css"))
	 @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Team/team.js"))
}
@using CodeStash.ExtensionsMethods


<div id="dialog-message"  style="display:none;">
	<p id="dialog-message-content">
	</p>
</div>

<input type="hidden" id="CurrentDraggablePosition" />

<div id="TeamPanel">

	
	<div class="headedPanel">
		<div class="stepPanel">
			<br />
			<h3>
				Step 1 : Create New Team, Or Pick Existing Team</h3>
			<p>
				<strong>Create A New Team</strong>
				<br />
				@Html.TextBoxFor(x => x.TeamName)
				<span class="btn"><a id="CreateTeam">Create Team</a><span></span></span>
				...
				...
				...
				<select id="OwnedTeams" style="display: none" class="selectBox">
				</select>
			</p>
		</div>
	</div>
	<div id="AssignToTeamPanel"  class="headedPanel" style="display:none">
		<div class="stepPanel">
			<br />
			<h3>
				Step 2 : Pick Your Team Members, And Assign To Selected Team</h3>
			<p>
				<strong>Search For User To Add To Selected Team</strong>
				<br />
				@Html.TextBoxFor(x => x.Email)
				<span class="btn"><a id="SearchForUsers">Search For Users</a><span></span></span>
				...
				...
				...
				<input id="IsOpenIdLogin" type="checkbox" />
				@Html.LabelFor(x => x.IsOpenIdLogin, "Search For OpenId Registered Users")
				...
				...
				...
				<div id="FoundUsersPanel" style="display: none">
					<strong>Users That Matched Your Search</strong>
					...
					...
					...

					<select id="FoundUsers" class="selectBox">
					</select>
					<div style="position: absolute;margin-top: -30px;margin-left: 205px;width: 170px;">
					<span class="btn"><a id="AssignUser">Assign User To Team</a><span></span></span>
					...
					...
					...
				</div>
			</p>
		</div>
	</div>
	<div id="TeamMembersPanel" class="headedPanel" style="display:none">
		<div class="stepPanel">
			<br />
			<h3>Step 3 : Remove People From The Selected Team (If You Must)</h3>
			<br />

			<div id="teamMemberContainer"></div>


			<script id="teamMemberTemplate" type="text/x-jQuery-tmpl">
				<div class="ui-widget-content draggable">
					<input type="hidden" class="draggableHidden" value="${UserId}" />
					<span class="username">${UserName}</span>
					<img src="../../Content/images/people.png" class="TeamPeopleIcon" />
					<img src="../../Content/images/delete.png" class="TeamDeleteIcon" />
					<div>
						<a href="mailto:${Email}">${UserName}</a>
					</div>
				</div>
				<div class="tooltip">
					You can drag ${UserName} to the bin to remove them from the current team
				</div>
			</script>

			<div class="ui-widget-header droppable">
			</div>
		</div>
	</div>
</div>

可以看出,屏幕的底部部分利用了很棒的 jQuery 模板 概念。我非常喜欢它。

好的,让我们继续看团队页面 JavaScript 最相关的部分。

$(document).ready(function () {

    GetOwnedTeams();

    $('.TeamDeleteIcon').live('click', function () {
        var id = $(this).parent().find('.draggableHidden').val();
        DeleteMemberFromTeam(id, undefined);
    });



    $('#CreateTeam').click(function () {

        var newTeamName = $('#TeamName').val();
        if (newTeamName == undefined || newTeamName == '') {
            $('#TeamName').addClass('Error');
        }
        else {
            $('#TeamName').removeClass('Error');
            $.post("/Team/SaveNewTeam", { teamName: newTeamName },
            function (data) {
                	....
                	....
                	....

            },
            "json");
        }
    });


    $(".droppable").droppable({
        hoverClass: "ui-state-active",
        drop: function (event, ui) {
            $(this).addClass("ui-state-highlight");

            var fullDragUI = $(ui.draggable.context);
            var id = fullDragUI.find('.draggableHidden').val();
            DeleteMemberFromTeam(id, ui.draggable);
        }
    });


    $('#AssignUser').click(function () {
        $.post("/Team/AssignMemberToTeam", { teamId: $('#OwnedTeams').val(), teamMemberId: $('#FoundUsers').val() },
        function (data) {
            if (data.Success != undefined && data.Success) {
                GetTeamMembersForSpecificTeam($('#OwnedTeams').val());
            }
            else {
                showOkDialog(data.Message, 180, 'Error');
            }
        },
        "json");
    });



    $('#OwnedTeams').change(function () {
        GetTeamMembersForSpecificTeam($('#OwnedTeams').val());
    });



    $('#SearchForUsers').click(function () {

        var emailToSearchFor = $('#Email').val();

        if (emailToSearchFor == undefined || emailToSearchFor == '') {
            $('#Email').addClass('Error');
        }
        else {
            $.post("/Team/SearchForUsers", { email: $('#Email').val(), isOpenIdlogin: $('#IsOpenIdLogin').is(':checked') },
                function (data) {
                    if (data.Success != undefined && data.Success) {
                 	....
                	....
                	....

                    }
                    else {
                        showOkDialog(data.Message, 180, 'Error');
                    }
                },
                "json");
        }
    });
});



function DeleteMemberFromTeam(userId, draggable) {

    showYesNoDialog('Are you sure you want to delete this user from the selected team?', 180, 'Confirm',
                function () {
                    $.post("/Team/DeleteMemberFromTeam", { teamId: $('#OwnedTeams').val(), teamMemberId: userId },
                	....
                	....
                	....
                        },
                        "json");
                },
                function () {
                    //animate draggable back currentDraggablePosition
                    AnimateDraggableBack(draggable);

                }
            );
}


function GetTeamMembersForSpecificTeam(teamId) {

    $("#teamMemberContainer").empty();

    $.post("/Team/GetTeamMembersForSpecificTeam", { teamId: teamId },
            function (data) {
                if (data.Success != undefined && data.Success) {
                    if (data.Message.length > 0) {
                        $("#teamMemberTemplate").tmpl(data.Message).appendTo("#teamMemberContainer");
                        $("#TeamMembersPanel").show();
                    }
                    else {
                        $("#TeamMembersPanel").hide();
                    }
                }
                else {
                    $("#TeamMembersPanel").hide();
                    showOkDialog(data.Message, 180, 'Error');
                }


                $(".draggable").tooltip({ effect: 'slide' });

                $(".draggable").draggable({

                    revert: 'invalid',
                    stop: function () {
                        $(this).draggable('option', 'revert', 'invalid');
                    },
                    start: function (event, ui) {
                        $('#CurrentDraggablePosition').val(ui.position);
                    },
                    drag: function (event, ui) {
                        $(".draggable").each(function () {
                            $(this).tooltip().hide();
                        });
                    }
                });

            },
            "json");
}




function GetOwnedTeams() {

    $.post("/Team/GetOwnedTeams",
            function (data) {
                if (data.Success != undefined && data.Success) {
                ....
                ....
                ....
                }
                else {
                    $('#OwnedTeams').hide();
                    $('#AssignToTeamPanel').hide();
                }
            },
            "json");
}



function AnimateDraggableBack(element) {

    if (element != undefined) {
        //animate draggable back currentDraggablePosition
        element.animate({
            left: $('#CurrentDraggablePosition').val(value).left,
            top: $('#CurrentDraggablePosition').val(value).top
        }, 600, "easeOutElastic");
    }

}

我把拖放功能留在那儿了,因为那是一些更有趣的 jQuery,它使用了出色的 jQuery UI 库,这真是太棒了。

现在,最后让我们看看主要的控制器方法,我希望您能看到上面 JavaScript 中正在调用它们。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Security;
using CodeStash.Common.DataAccess.EntityFramework;
using CodeStash.Common.DataAccess.Repository;
using CodeStash.Common.DataAccess.UnitOfWork;
using CodeStash.Common.Encryption;
using CodeStash.Filters;
using CodeStash.Models.Security;
using CodeStash.Services;
using CodeStash.Services.Contracts;




namespace CodeStash.Controllers
{
    public class TeamController : BaseTagCloudEnabledController
    {

        private readonly IMembershipService membershipService;
        private readonly IMembershipDataProvider membershipDataProvider;
        private readonly ILoggerService loggerService;
        private readonly IRepository<OwnedTeam> ownedTeamRepository;
        private readonly IRepository<CreatedTeam> createdTeamRepository;
        private readonly IUnitOfWork unitOfWork;



        public TeamController(IMembershipService membershipService,
            IMembershipDataProvider membershipDataProvider,
            ILoggerService loggerService, 
            ITagCloudService tagCloudService,
            IRepository<OwnedTeam> ownedTeamRepository, 
            IRepository<CreatedTeam> createdTeamRepository,
            IUnitOfWork unitOfWork)
            : base(tagCloudService)
        {
	....
	....
	....
        }



        [Authorize]
        [RenderTagCloud]
        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }


        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult SaveNewTeam(string teamName)
        {
	....
	....
	....

        }


        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult GetOwnedTeams()
        {
	....
	....
	....
        }



        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult GetTeamMembersForSpecificTeam(int teamId)
        {
	....
	....
	....
        }



        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult AssignMemberToTeam(int teamId, string teamMemberId)
        {
	....
	....
	....
        }



        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult DeleteMemberFromTeam(int teamId, string teamMemberId)
        {
	....
	....
	....

        }



        [Authorize]
        [HttpPost]
        [AjaxOnly]
        public ActionResult SearchForUsers(string email, bool isOpenIdlogin)
        {
	....
	....
	....

        }
    }
}

从网络打开

此页面允许您输入现有基于网络的片段的 URL,并对其进行高亮显示。本质上,所有需要做的就是解析源,并提取和高亮显示代码片段(如果可能)。

以下是 CodeSnippetController 中最相关的代码:

[Authorize]
[RenderTagCloud]
[HttpGet]
public ActionResult OpenFromWeb()
{
    OpenFromWebViewModel vm = new OpenFromWebViewModel();
    vm.HighlightingCSS = CodeSnippetUtils.GetUsersSavedHighlightingCSS(User.Identity);
    AddLanguagesToOpenFromWebVm(vm);
    return View(vm);
}


[Authorize]
[RenderTagCloud]
[HttpPost]
[AjaxOnly]
[ValidateAntiForgeryToken(Salt = "OpenFromWeb")]
public ActionResult OpenFromWeb(OpenFromWebViewModel vm)
{
    try
    {
        OpenFromWebViewModel newVm = new OpenFromWebViewModel();
        newVm.HighlightingCSS = CodeSnippetUtils.GetUsersSavedHighlightingCSS(User.Identity);
        newVm.ActualCode = CodeSnippetUtils.ReadContentFromWebUrl(vm.FileName);
        AddLanguagesToOpenFromWebVm(newVm);
        newVm.LanguageId = vm.LanguageId;
        newVm.CodeHasBeenParsed = ModelState.IsValid && !string.IsNullOrWhiteSpace(newVm.ActualCode);
        return PartialView("OpenFromWebPartial", newVm);
    }
    catch(Exception ex)
    {
        OpenFromWebViewModel newVm = new OpenFromWebViewModel();
        newVm.HighlightingCSS = CodeSnippetUtils.GetUsersSavedHighlightingCSS(User.Identity);
        AddLanguagesToOpenFromWebVm(newVm);
        newVm.CodeHasBeenParsed = false;
        return PartialView("OpenFromWebPartial", newVm);
    }
}

这段代码使用了这个实用代码

public static string ReadContentFromWebUrl(string url)
{
    try
    {
        System.Net.HttpWebRequest fr = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(new Uri(url));
        if ((fr.GetResponse().ContentLength > 0))
        {
            System.IO.StreamReader str = new System.IO.StreamReader(fr.GetResponse().GetResponseStream());
            return str.ReadToEnd();
        }
        return "";
    }
    catch (System.Net.WebException ex)
    {
        return "";
    } 
}

标记最相关的部分如下所示

@model CodeStash.Models.Snippet.OpenFromWebViewModel
@{
	ViewBag.Title = "Open From Web";
	Layout = "~/Views/Shared/_Layout.cshtml";
}

@using CodeStash.ExtensionsMethods


@section SpecificPageHeadStuff
 {
     @Html.CssTag(Url.Content("~/Content/Controllers/CodeSnippet/openFromWeb.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/CodeSnippet/openFromWeb.js"))
}



<div id="dialog-message"  style="display:none;">
	<p id="dialog-message-content">
	</p>
</div>

<div id="AddSnippetPanel">
    
    <h2>Open From Web</h2>
    @Html.Partial("OpenFromWebPartial",Model)

</div>

以下是相关的 JavaScript

$(document).ready(function () {

	InitBinding();
});



function InitBinding() {

 
	//On submit on add page, submit the add and shows the success
	//page it the edit was successful, otherwise add page is shown again including
	//validation errors
    $("#Submit").click(function (e) {
        e.preventDefault();
        CallPostRequestForOpen();
    });
}



//Ajax request to load in next page of results
function CallPostRequestForOpen() {

    var formData = $("#OpenFromWebForm").serialize();

    $.post("/CodeSnippet/OpenFromWeb", formData, function (response) {
        $("#AjaxContents").replaceWith(response);
	    InitBinding();

	    var successfulParse = $('#successfulParse').val();
	    if (successfulParse == "True") {
	        showOkDialog('Your parsed snippet is shown below', 180, 'Error');

	        SwitchHightlighting($("#HighlightingCSSToUse").val());

	    }
	    else {
	        showOkDialog('Could not parse your code snippet', 180, 'Error');
	    }
	});
	return false;
}

添加片段

添加片段本质上就是一天结束时的简单 INSERT 语句。它看起来像这样:

此页面允许登录用户创建新的代码片段

视图如下所示:

@model CodeStash.Models.Snippet.AddSnippetViewModel
@using CodeStash.ExtensionsMethods

<div id="AjaxAddContents">
    @using (Html.BeginForm("Add", "CodeSnippet", FormMethod.Post, new { id = "AddForm" }))
    {
        @Html.AntiForgeryToken("Add")
        
        <h2>Add A New Code Snippet</h2>
        <p>Please fill in the details below, and when ready click the "Add" button at the bottom of the page</p>


        
        <input id="successfulAdd" type="hidden" value="@ViewData["successfulAdd"]" />
        <input id="addedSnippetId" type="hidden" value="@ViewData["addedSnippetId"]" />
        
        <div class="headedPanel">
            <strong>Title</strong>
            <div>
                @Html.TextBoxFor((x) => x.Title)
                 
                @Html.ValidationMessageFor((x) => x.Title)
            </div>
        </div>
        
        <div class="headedPanel">
            <strong>Description</strong>
            <div>
                @Html.TextBoxFor((x) => x.Description)
                 
                @Html.ValidationMessageFor((x) => x.Description)
            </div>
        </div>
        
        <div class="headedPanel">
            <strong>Category</strong>
            <div>
                Search for existing category
                <br />
                <input id="SearchCategoryText" type="text" />
                <span class="btn"><a id="SearchForExistingCategories">Search</a><span></span></span>
                <span class="clear"></span>
                <select id="FoundCategories" style="display: none" class="selectBox">
                </select>
                <br />
                Or fill in new category
                <br />
                @Html.TextBoxFor((x) => x.NewCodeCategoryName)
                 
                @Html.ValidationMessageFor((x) => x.NewCodeCategoryName)
            </div>
        </div> 
   
        
        <div class="headedPanel">
            <strong>Grouping</strong>
            <div>
                Search for existing grouping
                <br />
                <input id="SearchGroupingText" type="text" />
                <span class="btn"><a id="SearchForExistingGrouping">Search</a><span></span></span>
                <span class="clear"></span>
                <select id="FoundGrouping"  style="display: none" class="selectBox">
                </select>
                <br />
                Or fill in new grouping
                <br />
                @Html.TextBoxFor((x) => x.NewGroupingName)
                 
                @Html.ValidationMessageFor((x) => x.NewGroupingName)
            </div>
        </div>
        
        <div class="headedPanel">
            <strong>Language</strong>
            <div>
                @Html.ComboFor(x => x.LanguageId, 
                x => x.LanguageList, 
                x => x.LanguageId, 
                x => x.Language1, 
                    new Dictionary<string,object> { 
                        { "style", "width:400px"}, 
                        { "class", "selectBox"  }})
                 
                @Html.ValidationMessageFor((x) => x.LanguageId)
            </div>
        </div>
        
        <div class="headedPanel">
            <strong>Code Snippet Visibility</strong>
            <div>
                    @Html.ComboFor(x => x.Visibility, 
                    x => x.VisibilityList, 
                    x => x.Id, 
                    x => x.VisibilityDescription,
                    new Dictionary<string, object> { 
                        { "style", "width:400px"}, 
                        { "class", "selectBox"  }})
                 
                @Html.ValidationMessageFor((x) => x.Visibility)
            </div>
        </div>
        
        <div class="headedPanel">
            <div>
                <strong>Tags</strong>     Enter tags seperated by ";"
                <div>
                    @Html.TextBoxFor((x) => x.Tags)
                     
                    @Html.ValidationMessageFor((x) => x.Tags)
                </div>
            </div>
        </div>
        
        <div class="headedPanel">
            <strong>Actual Code</strong>
            <div>
                @Html.TextAreaFor((x) => x.ActualCode, new { id = "editTextFieldsCode" })
                 
                <div id="actualCodeError">@Html.ValidationMessageFor((x) => x.ActualCode)</div>
            </div>
        </div>
        
        <span class="btn"><a id="AddSubmit">Add</a><span></span></span>
        <span class="clear"></span>
        <br />
        <br />        
                
        
    }
</div>

JavaScript 最相关的部分如下所示,其中表单提交给 CodeSnippet 控制器的 Add 操作。

//Ajax request to load in next page of results
function CallPostRequestForAddConfirmSnippet() {

	var formData = $("#AddForm").serialize();

	$.post("/CodeSnippet/Add", formData, function (response) {
	    $("#AjaxAddContents").replaceWith(response);
	    InitBinding();

	    var successfulAdd = $('#successfulAdd').val();
	    if (successfulAdd == "True") {
	        var addedSnippetId = $('#addedSnippetId').val();
	        window.location.href = '/CodeSnippet/DisplaySnippetsForAddAndEdit' 
            + '?codeSnippetId=' + addedSnippetId;
	    }
	    else {
	        showOkDialog('Could not save your code snippet', 180, 'Error');
	    }
	});
	return false;
}

其中 CodeSnippet 控制器的 Add 操作如下所示:

[Authorize]
[HttpPost]
[ValidateInput(false)] // allow through code type text for this Action
[AjaxOnly]
[ValidateAntiForgeryToken(Salt = "Add")]
public ActionResult Add(AddSnippetViewModel vm)
{
    try
    {
        if (ModelState.IsValid)
        {
            CodeSnippet addedSnippet = AddOrUpdateSnippet(vm, false);
            ViewData["successfulAdd"] = true;
            ViewData["addedSnippetId"] = addedSnippet.CodeSnippetId;
            RefreshAddModelStaticData(vm);
            return PartialView("AddSnippetPartial", vm);
        }
        else
        {
            ViewData["successfulAdd"] = false;
            ViewData["addedSnippetId"] = 0;
            RefreshAddModelStaticData(vm);
            return PartialView("AddSnippetPartial", vm);
        }

    }
    catch
    {
        RefreshAddModelStaticData(vm);
        ViewData["successfulAdd"] = false;
        return PartialView("AddSnippetPartial", vm);
    }
}

唯一需要注意的另一点是,使用了 System.DataAnnotations 命名空间来实现表单验证。事实上,DataAnnotations 在整个 CodeStash 中都有使用。

这是一个它们如何显示的例子

public class AddSnippetViewModel : ISnippetViewModel
{
    #region Ctor
    /// <summary>
    /// For Post request where ModelBinding takes care of matching up properties for us
    /// </summary>
    public AddSnippetViewModel()
    {
        LanguageList = new List<Language>();
        VisibilityList = new List<Visibility>();

    }
    #endregion

    #region Public Properties

    public List<Language> LanguageList { get; set; }

    [Required(ErrorMessage = "You must enter a value for Language")]
    public int LanguageId { get; set; }

    public int CodeCategoryId { get; set; }


    [Required(ErrorMessage = "You must enter a value for Category")]
    public string NewCodeCategoryName { get; set; }


    public int GroupId { get; set; }


    [StringLength(100, MinimumLength = 0,
        ErrorMessage = "Grouping must be between 0-100 characters in length")]
    public string NewGroupingName { get; set; }


    [StringLength(100, MinimumLength=0,
        ErrorMessage = "Tags must be between 0-100 characters in length")]
    public string Tags { get; set; }


    [Required(ErrorMessage = "You must enter a value for Description")]
    public string Description { get; set; }


    [Required(ErrorMessage = "You must enter a value for Title")]
    public string Title { get; set; }


    [Required(ErrorMessage = "You must enter a value for ActualCode")]
    public string ActualCode { get; set; }


    public List<Visibility> VisibilityList { get; set; }

    [Required(ErrorMessage = "You must enter a value for Visibility")]
    public int Visibility { get; set; }
        

    public Guid AspNetMembershipUserId { get; set; }

    #endregion
}

片段成功添加后,它将与其组中的任何其他片段(如果是在组中创建的)一起显示。

搜索片段

搜索片段显然是 CodeStash 的核心功能之一,正如我之前所述,CodeStash 支持多种搜索片段的方法,例如:

  • 按标签
  • 按关键词
  • 按语言

所有这些都可以通过可见性修饰符进一步限制。搜索屏幕如下所示:

可以看出,这个页面只是允许您设置所需的搜索。我稍后将深入探讨搜索的工作原理,但现在让我们先看看一两个其他屏幕截图。

当您点击开始异步搜索的按钮时,将显示一个进度轮。

搜索完成后,将显示结果页面,其中包含一个 DataGrid(或一条消息,指出“找不到任何结果”)。

可以看出(请记住,您可以单击这些图像以查看更大的版本),有一个结果 DataGrid(我正在使用免费的 TTelerik ASP MVC Extensions DataGrid),其中有两列带有超链接,它们如下所示:

  • 弹出窗口:将显示一个弹出对话框,其中显示片段
  • 显示:将实际使用下面显示的 显示片段 页面显示片段。

以下是当您单击网格“弹出”列中的超链接时将显示的内容示例:

如果用户点击“显示”超链接,他们将被定向到 显示片段 页面,该页面将显示选定的片段以及该组中的任何其他片段(如果选定的片段实际属于某个组)。

这就是“搜索”如何协同工作的屏幕截图,现在让我们深入探讨它的核心。

我想我们首先应该从搜索页面如何允许用户搜索开始,这是一个非常简单的表单,使用了以下标记:

@model CodeStash.Models.Search.CreateSearchViewModel
@using CodeStash.ExtensionsMethods
@{
	ViewBag.Title = "Create Search";
	Layout = "~/Views/Shared/_Layout.cshtml";
}

@section SpecificPageHeadStuff
 {
     @Html.CssTag(Url.Content("~/Content/Controllers/Search/search.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Search/search.js"))
}


<div id="dialog-message"  style="display:none;">
	<p id="dialog-message-content">
	</p>
</div>


<div id="SearchPanel">
 @using (Html.BeginForm("CreateSearch", "Search", FormMethod.Post, new { id = "CreateSearchForm" }))
    {
     
        @Html.AntiForgeryToken("CreateSearch")
        
        <div id="allFormData">
        
            <input id="successfulCreate" type="hidden" value="@ViewData["successfulCreate"]" />

        
 	        <h2>Create Your Search</h2>
	        <p>You can create your search by choosing and filling one of the sections below, and then cliking the
            submit button, where you will be redirected to the search results.</p>
        

            <div class="headedPanel">
		        <div class="stepPanel">

                    <label for="searchType_ByTag">ByTag</label>
                    @Html.RadioButtonFor(x => x.SearchType, "ByTag", new { id = "searchType_ByTag" })

                    <label for="searchType_ByKeyWord">ByKeyWord</label>
                    @Html.RadioButtonFor(x => x.SearchType, "ByKeyWord", new { id = "searchType_ByKeyWord" })

                    <label for="searchType_ByLanguage">ByLanguage</label>
                    @Html.RadioButtonFor(x => x.SearchType, "ByLanguage", new { id = "searchType_ByLanguage" })


			        <br />
                    <br />
                    <strong>Tag</strong>
                    <div>
                        @Html.TextBoxFor((x) => x.SearchForTag, new { @class = "textbox" })
                         
                        @Html.ValidationMessageFor((x) => x.SearchForTag)
                    </div>
                
                    <strong>Key Word</strong>
                    <div>
                        @Html.TextBoxFor((x) => x.SearchForKeyWord, new { @class = "textbox" })
                         
                        @Html.ValidationMessageFor((x) => x.SearchForKeyWord)
                    </div>

                    <strong>Language</strong>
                    <div>
                        @Html.ComboFor(x => x.LanguageId, 
			    x => x.LanguageList, 
			    x => x.LanguageId, 
			    x => x.Language1, 
                            new Dictionary<string,object> { 
                                { "style", "width:400px"}, 
                                { "class", "selectBox"  }})
                         
                        @Html.ValidationMessageFor((x) => x.LanguageId)
                    </div>

                </div>
	        </div>

 
            <div class="headedPanel">
        	    <div class="stepPanel">
			        <br />
                    <strong>Visibility<strong>
                    <div>
                     @Html.ComboFor(x => x.Visibility, 
			x => x.VisibilityList, 
			x => x.Id, 
			x => x.VisibilityDescription,
                        new Dictionary<string, object> { 
                            { "style", "width:400px"}, 
                            { "class", "selectBox"  }})
                     
                    @Html.ValidationMessageFor((x) => x.Visibility)
                    </div>
                </div>
            </div>
        
        
            <div style="margin-top:20px;margin-left:25px">
                <span class="btn"><a id="SearchSubmit">Submit</a><span></span></span>
                <span class="clear"></span>
                <br />
                <br /> 
            </div>
        
        </div>
 
        <div id="loader" style="display:none">
            @Html.Partial("_Loader")
        </div>
     
    }
</div>

那个表单非常简单,它基本上只提供了表单元素,允许用户输入他们所需的搜索,该搜索被 POST 到 SearchControllerCreateSearch 动作,我们稍后会看到。

现在让我们将注意力转向此页面的 JavaScript,它除了将表单提交到 SearchControllerSearch 动作外,并没有做太多事情。

$(document).ready(function () {

	InitBinding();
});



function InitBinding() {

    //	On submit on add page, submit the add and shows the success
    //	page it the edit was successful, otherwise add page is shown again including
    //	validation errors
    $("#SearchSubmit").click(function (e) {
        e.preventDefault();

        $("#CreateSearchForm").submit();
        $('#allFormData').hide();
        $('#loader').show();
    });


    if ($('successfulCreate').length > 0) {
        var successfulCreate = $('#successfulCreate').val();
        if (successfulCreate == "False") {
            showOkDialog('Your search is invalid', 180, 'Error');
        }
    }

}

好的,JavaScript 确实做了更多事情来处理 Telerik ASP MVC Extensions DataGrid,但我们稍后会看到额外的部分。

那么现在让我们看看 SearchControllerCreateSearch 操作中发生了什么。这里有几点需要注意,如下所示:

  • 由于搜索可能需要一段时间,我决定使用任务并行库异步执行此操作。
  • 由于我正在异步进行一些工作,因此此控制器继承自 AsyncController

令人惊讶的是,SearchController 在这里并不那么糟糕,以下是最相关的部分:

public class SearchController : BaseTagCloudEnabledAsyncController
{
    public SearchController(
        ILoggerService loggerService, 
        IMembershipService membershipService,
        ITagCloudService tagCloudService,
        IRepository<Language> languageRepository, 
        IRepository<Visibility> visibilityRepository,
        IRepository<CodeCategory> categoryRepository,
        IRepository<Grouping> groupingRepository,
        IRepository<CodeSnippet> codeSnippetRepository,
        IRepository<CodeTag> codeTagRepository,
        IRepository<CreatedTeam> createdTeamRepository,
        IUnitOfWork unitOfWork)
        : base(tagCloudService)
    {
        .....
    }



    [Authorize]
    [RenderTagCloud]
    [HttpPost]
    [ValidateAntiForgeryToken(Salt = "CreateSearch")]
    public void CreateSearchAsync(CreateSearchViewModel vm)
    {
        AsyncManager.OutstandingOperations.Increment();

        if (ModelState.IsValid)
        {
            ViewData["successfulCreate"] = true;
            try
            {
                MembershipUser user = membershipService.GetUserByUserName(User.Identity.Name);
                Guid userId = Guid.Parse(user.ProviderUserKey.ToString());
                SearchTaskState searchTaskState = new SearchTaskState(ViewData, vm, userId);

                Task<ShowSearchResultsViewModel> searchTask = CreateSearchTask(searchTaskState);
                searchTask.Start();
                searchTask.ContinueWith((ant) =>
                {
                    SetValidSearchAsyncResult(AsyncManager, ant.Result);
                    AsyncManager.OutstandingOperations.Decrement();
                }, TaskContinuationOptions.OnlyOnRanToCompletion);

                searchTask.ContinueWith((ant) =>
                {
                    SetInvalidAsyncResult(AsyncManager, vm);
                    AsyncManager.OutstandingOperations.Decrement();
                }, TaskContinuationOptions.OnlyOnFaulted);

            }
            catch (AggregateException ex)
            {
                SetInvalidAsyncResult(AsyncManager, vm);
                AsyncManager.OutstandingOperations.Decrement();
            }
        }
        else
        {
            SetInvalidAsyncResult(AsyncManager, vm);
            AsyncManager.OutstandingOperations.Decrement();
        }

    }

    public ActionResult CreateSearchCompleted(bool wasValid, object vm)
    {
        if (!wasValid)
        {
            return View("CreateSearch", vm);
        }
        else
        {
            return View("ShowSearchResults", vm);
        }
    }


    private void SetInvalidAsyncResult(AsyncManager asyncManager, CreateSearchViewModel vm)
    {
        ViewData["successfulCreate"] = false;
        using (unitOfWork)
        {
            RefreshModelStaticData(vm);
        }
        asyncManager.Parameters["wasValid"] = false;
        asyncManager.Parameters["vm"] = vm;
    }


    private void SetValidSearchAsyncResult(AsyncManager asyncManager, ShowSearchResultsViewModel vm)
    {
        AsyncManager.Parameters["wasValid"] = true;
        AsyncManager.Parameters["vm"] = vm;
    }




    private Task<ShowSearchResultsViewModel> CreateSearchTask(SearchTaskState searchTaskState)
    {
        return new Task<ShowSearchResultsViewModel>((state)=>
            {
                SearchTaskState taskState = (SearchTaskState)state;
                ShowSearchResultsViewModel searchResultsVm;

                using (unitOfWork)
                {
                    .....
                    .....
                    .....
                    .....
                    Tuple<int, List<CodeSnippet>> results =
                        SearchUtils.FilterByVisibility(
                            unitOfWork,
                            createdTeamRepository,
                            codeTagRepository,
                            languageRepository,
                            codeSnippetRepository,
                            taskState.Vm.SearchType,
                            searchValue.ToLower(),
                            1,
                            1,
                            false,
                            visibility,
                            taskState.UserId,
                            tags);

                    Session["searchResults"] = results.Item2;

                    searchResultsVm =
                        new ShowSearchResultsViewModel(
                            taskState.Vm.SearchType,
                            languageRepository.FindBy(x => x.LanguageId == taskState.Vm.LanguageId).Single(),
                            visibilityRepository.FindBy(x => x.Id == taskState.Vm.Visibility).Single(),
                            searchValue,
                            results.Item1,
                            results.Item2);
                }
                return searchResultsVm;
            }, searchTaskState);


    }



    private string GetSearchValueBasedOnSearchType(CreateSearchViewModel vm)
    {
        string searchValue = "";
        switch (vm.SearchType)
        {
            case SearchType.ByKeyWord:
                searchValue = vm.SearchForKeyWord;
                break;
            case SearchType.ByTag:
                searchValue = vm.SearchForTag;
                break;
            case SearchType.ByLanguage:
                languageRepository.EnrolInUnitOfWork(unitOfWork);
                searchValue = languageRepository.FindBy(x => x.LanguageId == vm.LanguageId).Single().Language1;
                break;
        }
        return searchValue;
    }
}

可以看出,一旦获得搜索结果,该控制器实际上会返回一个名为 ShowSearchResultsView 的新视图,该视图如下所示,这就是我们之前看到的带有 Telerik ASP MVC Extensions DataGrid 的屏幕截图。

@model CodeStash.Models.Search.ShowSearchResultsViewModel
@using Telerik.Web.Mvc.UI
@using CodeStash.ExtensionsMethods
@{
    ViewBag.Title = "Create Search";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section SpecificPageHeadStuff
 {  
     @Html.CssTag(Url.Content("~/Content/Controllers/Search/search.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Search/search.js"))
}

<div id="dialog-message" style="display: none;">
    <p id="dialog-message-content">
    </p>
</div>
<div id="SearchPanel">

    <h2>Showing Search Results</h2>
    <p>Your search was</p>
    <ul>
        <li>Visibility : '@Model.Visibility.VisibilityDescription'</li>
        @switch (Model.SearchType)
        {
            case CodeStash.Common.Enums.SearchType.ByKeyWord:
                   <li>Search Type : 'ByKeyWord', where your search term was :  '@Model.SearchValue'</li>
                    break;
            case CodeStash.Common.Enums.SearchType.ByLanguage:
                   <li>Search Type : 'ByLanguage', where your search language was : '@Model.Language.LanguageCode'</li>
                    break;
            case CodeStash.Common.Enums.SearchType.ByTag:
                   <li>Search Type : 'ByTag', where your search term was : '@Model.SearchValue'</li>
                   break;
        }
    </ul>


    @(Html.Telerik().Grid<CodeStash.Common.DataAccess.EntityFramework.CodeSnippet>()
        .ScriptFilesPath("~/Scripts/Telerik")
        .HtmlAttributes(new { style = "margin:0px" })
        .Name("SearchResultsGrid")
            .DataBinding(dataBinding => dataBinding
                //Ajax binding
                .Ajax()
                //The action method which will return JSON
                .Select("_AjaxBinding", "Search")
            )
            .Columns(columns =>
            {

                columns.Bound(o => o.CodeSnippetId).Title("Id").HeaderHtmlAttributes(new { @class = "id-column" }).ReadOnly(true);
                columns.Bound(o => o.Description).ReadOnly(true);
                columns.Bound(o => o.Title).ReadOnly(true);
                columns.Bound(o => o.CodeSnippetId).ClientTemplate("<a href=\"#\" class=\"popup-button\">View Details</a>")
                    .Title("Popup").Filterable(false).Sortable(false).Width(130).ReadOnly(true);
                columns.Bound(o => o.CodeSnippetId).ClientTemplate("<a href=\"#\" class=\"show-button\">View Details</a>")
                    .Title("Show").Filterable(false).Sortable(false).Width(130).ReadOnly(true);
            })
            .ClientEvents(e => e.OnRowDataBound("SearchResultsGrid_onRowDataBound"))
            .Pageable()
            .Scrollable(scroll => scroll.Height(200))
            .Filterable()
            .Sortable(sorting => sorting
            .SortMode(GridSortMode.SingleColumn)
                .OrderBy(o => o.Add(p => p.CodeSnippetId).Ascending()))

    )
</div>

可以看出,这使用了 Telerik ASP MVC Extensions(完全免费,感谢 Telerik)。

此网格支持各种不同的模型,但我选择的模型是 Ajax 更新模型,它允许 DataGrid 使用服务器端方法进行更新,该方法在分页发生时被调用。此服务器端控制器方法如下所示。

[GridAction]
public ActionResult _AjaxBinding()
{
    return View(new GridModel((IEnumerable)Session["searchResults"]));
}

为了使这个免费的 Telerik DataGrid 正常工作,我们需要在主页面“_Layout.cshtml”中做一些事情,如下所示:

@( Html.Telerik().StyleSheetRegistrar().DefaultGroup(group => group
    .DefaultPath("~/Content/Telerik")
    .Add("telerik.common.css")
    .Add("telerik.Black.min.css"))
)

搜索的最后一部分是其余的 JavaScript,我故意没有提前向您展示,现在它显示在下面。可以看出,JavaScript 负责绑定两个超链接网格列,并且还包含 jQuery Ajax 调用,用于在单击 DataGrid 超链接列时重定向或获取单个片段代码。

//Telerik grid setup
function SearchResultsGrid_onRowDataBound(e) {
    var dataItem = e.dataItem;
    var snippetId = dataItem.CodeSnippetId;

    $(e.row).find("a.popup-button")
                    .click(function (e) {
                        ShowSnippetPopup(snippetId);
                    });

    $(e.row).find("a.show-button")
                .click(function (e) {
                    window.location.href = '/CodeSnippet/DisplaySnippetsForAddAndEdit' + 
			'?codeSnippetId=' + snippetId + "&wasAddOrEdit=false";
                });
}


function ShowSnippetPopup(snippetId) {


    $.post("/Search/GetSpecificSnippetData", { snippetId: snippetId },
            function (data) {
                if (data.Success != undefined && data.Success) {

                    var codeSnippetContents = '<pre id="preCode_' + snippetId + '" class="' + 
			data.PreClass + ' searchsnippetPopup">' + data.CodeSnippetCode + '</pre>'
                    	showDialogWidthAndHeightNoCallbackAndNoScroll(codeSnippetContents,
                        600, 450, 'Displaying Snippet : ' + snippetId);
                    SwitchHightlighting(data.HighlightingCSSName);
                }
                else {
                    showOkDialog(data.Message, 180, 'Error');
                }
            },
            "json");

}

这就是搜索的工作原理

显示片段

片段的显示由 CodeSnippetController 处理,其中有 2 个主要方法处理片段的显示。

  • DisplaySnippetsForAddAndEdit:此操作在成功添加或编辑后被调用,并将显示添加的片段或编辑的片段。如果该片段是某个组的一部分,则该组中的所有片段也将显示。
  • DisplaySnippetsForCategory:当用户单击标签云中的某个条目时,将调用此操作。
namespace CodeStash.Controllers
{
	public class CodeSnippetController : BaseTagCloudEnabledController
	{

	        [Authorize]
        	[RenderTagCloud]
        	[HttpGet]
        	public ActionResult DisplaySnippetsForAddAndEdit(int codeSnippetId, bool wasAddOrEdit=true)
        	{
		....
		....
		....
		....

		}

 



	        [Authorize]
       	 	[RenderTagCloud]
        	[HttpGet]
        	public ActionResult DisplaySnippetsForCategory(string category)
        	{
		....
		....
		....
		....

		}
}

在任何一种情况下都会显示 DisplaySnippets 完整页面视图,并向其传递以下 ViewModel:

namespace CodeStash.Models.Snippet
{
    public class DisplaySnippetsViewModel
    {
        public static int MAX_SNIPPETS_TO_DISPLAY = 100;

        public DisplaySnippetsViewModel(
            List<CodeSnippetWrapperViewModel> codeSnippets, 
            DisplayMode displayMode,
            bool isTruncated,
            String highlightingCSS)
        {
            this.CodeSnippets = codeSnippets;
            this.DisplayMode = displayMode;
            this.IsTruncated = isTruncated;
            this.HighlightingCSS = highlightingCSS;
            IsGrouped = this.CodeSnippets.Any(x => x.CodeSnippet.GroupId.HasValue);

        }


        public bool IsGrouped { get; private set; }
        public List<CodeSnippetWrapperViewModel> CodeSnippets { get; private set; }
        public DisplayMode DisplayMode { get; private set; }
        public bool IsTruncated { get; private set; }
        public string HighlightingCSS  { get; private set; }

    }



    public class CodeSnippetWrapperViewModel
    {
        public bool IsSnippetEditable { get; private set; }
        public CodeSnippet CodeSnippet { get; private set; }
 
        public CodeSnippetWrapperViewModel(bool isSnippetEditable, CodeSnippet codeSnippet)
        {
            this.IsSnippetEditable = isSnippetEditable;
            this.CodeSnippet = codeSnippet;


        }
    }
}

这是完整页面视图的标记,看看它是如何显示另一个局部视图 DisplaySnippetsPartial 的。

@model CodeStash.Models.Snippet.DisplaySnippetsViewModel
@{
	ViewBag.Title = "Grouped Snippets";
	Layout = "~/Views/Shared/_Layout.cshtml";
}
@using CodeStash.ExtensionsMethods

@section SpecificPageHeadStuff
 {
     @Html.CssTag(Url.Content("~/Content/Controllers/DisplaySnippets/displaySnippets.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/DisplaySnippets/displaySnippets.js"))

}



<div id="dialog-message"  style="display:none;">
	<p id="dialog-message-content">
	</p>
</div>

<div id="DisplaySnippetPanel">
    
    <h2>Code Snippets</h2>
    <input id="WasAddOrEdit" type="hidden" value="@ViewData["WasAddOrEdit"]" />
    <input id="DisplayMode" type="hidden" value="@Model.DisplayMode" />
    <input id="HighlightingCSSToUse" type="hidden" value="@Model.HighlightingCSS" />
    @Html.Partial("DisplaySnippetsPartial",Model)
    


</div>

以下是局部视图 DisplaySnippetsPartial 最相关的部分:

@model CodeStash.Models.Snippet.DisplaySnippetsViewModel
           
<div id="AjaxContents">
<p id="noSnippetsMessage" style="display:none;">There are no CodeSnippets to display</p>

@if (Model.DisplayMode == CodeStash.DisplayMode.SingleSnippet)
{
    if (Model.IsGrouped)
    {
        <p id="snippetsMessage">As the requested snippet is part of a group, displaying all Code Snippets in Group 
        [@Model.CodeSnippets.First().CodeSnippet.Grouping.Description]</p>
    }
    else
    {
        <p id="snippetsMessage">The requested snippet is shown below</p>
    }
}

@foreach (CodeStash.Models.Snippet.CodeSnippetWrapperViewModel snippet in Model.CodeSnippets)
{
    @Html.Partial("SingleSnippetPartial", snippet);
}
    
    
</div>

可以看出,这个局部视图还使用了另一个局部视图 SingleSnippetPartial,顾名思义,它负责渲染单个片段。SingleSnippetPartial 和随附的 JavaScript 提供了以下功能:

  • 折叠当前片段
  • 展开当前片段
  • 编辑当前片段
  • 删除当前片段
  • 提供当前片段的可共享链接(共享后允许非 CodeStash 用户查看只读版本的片段)

无论如何,这就是 SingleSnippetPartial 标记的样子:

@model CodeStash.Models.Snippet.CodeSnippetWrapperViewModel

<div id="codeSnippet-@Model.CodeSnippet.CodeSnippetId" class="snippet">
    <p>
        <strong>ID:&nbsp</strong>@Model.CodeSnippet.CodeSnippetId<br />
        <strong>Title:&nbsp</strong>@Model.CodeSnippet.Title<br />
        <strong>Description:&nbsp</strong>@Model.CodeSnippet.Description<br />
        <strong>Category:&nbsp</strong>@Model.CodeSnippet.CodeCategory.CodeCategoryName<br />
    </p>


    <div class="highlightedSnippetHeader">
    

            <div class="snippetActionArea link">
                <img src="../../Content/images/link.png" width="25px" alt="" />
            </div>
            <div class="tooltip">Get link for this code snippet</div>


            <div class="snippetActionArea collapse">
                <img src="../../Content/images/collapse.png" width="25px" alt="" />
            </div>
            <div class="tooltip">Collapse code snippet</div>

            <div class="snippetActionArea expand">
                <img src="../../Content/images/expand.png" width="25px"  alt=""/>
            </div>
            <div class="tooltip">Expand code snippet</div>

            @if (Model.IsSnippetEditable)
            {
                <div class="snippetActionArea delete enabled">
                    <img src="../../Content/images/delete.png" width="25px" alt=""/>
                </div>
                <div class="tooltip">Delete code snippet</div>
        
                <div class="snippetActionArea edit enabled">
                    <img src="../../Content/images/edit.png" width="25px"  alt=""/>
                </div>
                <div class="tooltip">Edit code snippet</div>
            }
            else
            {
                <div class="snippetActionArea delete disabled">
                    <img src="../../Content/images/deleteDisabled.png" width="25px" alt=""/>
                </div>
                <div class="tooltip">Delete code snippet</div>
        
                <div class="snippetActionArea edit disabled">
                    <img src="../../Content/images/editDisabled.png" width="25px"  alt=""/>
                </div>
                <div class="tooltip">Edit code snippet</div>
            }
    </div>
    <div class="highlightedSnippet">
    @if (Model.CodeSnippet.Grouping != null)
    {
        <input class="isGrouped" type="hidden" value="true" />
        <input class="groupId" type="hidden" value="@Model.CodeSnippet.GroupId" />
        <input class="groupDescription" type="hidden" value="@Model.CodeSnippet.Grouping.Description" />
        
    }
    else
    {
        <input class="isGrouped" type="hidden" value="false" />
    }
    <input class="codeSnippetId" type="hidden" value="@Model.CodeSnippet.CodeSnippetId" />
    <pre class="@CodeStash.Utils.WebSiteUtils.GetCodeClass(Model.CodeSnippet.Language.LanguageCode)">
        @Model.CodeSnippet.ActualCode.Trim()</pre>
</div>
    <br />
</div>

无论如何,这是显示片段的最终结果

可以看出,有许多图片作为按钮,可以执行各种功能,例如折叠/展开/编辑/删除/分享链接,我们稍后在查看 JavaScript 方面时会深入探讨。

但是片段高亮是怎么回事呢?嗯,这实际上非常简单,我们只需确保渲染的代码包裹在一个具有部分由存储的片段语言组成的类的 PRE 中,因为我们将该信息存储在数据库中,所以这些信息可以自由获取。

所以这最终会得到一个像这样的 PRE:

<pre class="csharpCode">

这只是单个代码片段层面需要做的事情,当然这并不是全部,我们还使用了一个第三方免费 JavaScript 库来实际实现高亮显示。

在我最初构建的 CodeStash 原型中,我花了很长时间寻找最佳的语法高亮方法,幸运的是我找到了一个非常好的 JavaScript 语法高亮库,可从以下网址获取:

http://www.steamdev.com/snippet/

这个库附带了所有必需的文件,下面显示了使用它所需的步骤。尽管它在网站上也有非常好的教程,请参阅网站的“用法”部分。


CSS

包含指向 jquery.snippet.css 文件的链接。最好的位置是 MasterPage "_Layout.cshtml"

@Html.CssTag(Url.Content("~/Content/Highlighting/jquery.snippet.css"))

jQuery Snippet 插件

包含 jquery.snippet.js 文件的链接。最佳位置是 MasterPage "_Layout.cshtml"。

@Html.ScriptTag(Url.Content("~/Scripts/Highlighting/jquery.snippet.js"))

最后,以下是用于片段高亮的 JavaScript 最相关的部分。

可以看出,它包含了所有前面提到的功能(折叠/展开/编辑/删除/共享片段链接)的钩子。

$(document).ready(function () {

    InitBinding();
});


function InitBinding() {

    SwitchHightlighting($("#HighlightingCSSToUse").val());

    $(".link img").click(function () {

        var snippetElement = $(this).closest(".highlightedSnippetHeader").next(".highlightedSnippet");
        var codeSnippetId = snippetElement.find(".codeSnippetId").val();

        showNoButtonsDialog(
                '<p><strong>Snippet link</strong><br/><i>' + window.location.origin +
                '/Readonly/Display/' + codeSnippetId + '</i><br/><br/>' +
                'Copy the link to share it' +
                '</p>', 195, 'Information'
            );
    });


    $(".delete.enabled img").click(function () {

        var displayMode = $('#DisplayMode').val();
        var snippetElement = $(this).closest(".highlightedSnippetHeader").next(".highlightedSnippet");
        var isGrouped = snippetElement.find(".isGrouped").val();
        var groupDescription = snippetElement.find(".groupDescription").val();
        var groupId = snippetElement.find(".groupId").val();
        var codeSnippetId = snippetElement.find(".codeSnippetId").val();

        if (isGrouped == "true") {

            showYesNoGroupSnippetDialog("Code snippet " + codeSnippetId + " is part of group '" + groupDescription +
                "'.<br/><br/> Please pick whether to delete just the single selected snippet or ALL snippets in the group",
                220, "Confirm Delete",
                function (data) {
                    DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, false);
                },
                function (data) {
                    DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, true);
                }
            );
        }
        else {
            showYesNoDialog('Are you sure you want to delete this snippet?', 180, 'Confirm',
                function () {
                    DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, false);
                }
            );
        }
    });


    $(".edit.enabled img").click(function () {

        var snippetElement = $(this).closest(".highlightedSnippetHeader").next(".highlightedSnippet");
        var codeSnippetId = snippetElement.find(".codeSnippetId").val();
        window.location.href = '/CodeSnippet/Edit' + '?codeSnippetId=' + codeSnippetId;

    });

}

function DeleteSnippetAndShowRemaining(displayMode, snippetId, groupId, deleteAllInGroup) {

    $.post("/CodeSnippet/DeleteSnippetAndShowRemaining",
            {
                'displayMode': displayMode,
                'snippetId': snippetId,
                'groupId': groupId,
                'deleteAllInGroup': deleteAllInGroup
            },
            function (response) {
            ....
            ....
            ....
            ....
            ....
            },
            'json'
    );
}

在该 jQuery 的顶部附近有一行代码,用于使用用户选择的高亮片段主题(存储在 ASP .NET Membership 数据库的个人资料中)高亮片段。这行代码是:

SwitchHightlighting($("#HighlightingCSSToUse").val());

其中 SwitchHightlighting 方法如下所示:

function SwitchHightlighting(stylename) {

    $("pre.c" + "Code").snippet("c", { style: stylename, showNum: false });
    $("pre.cpp" + "Code").snippet("cpp", { style: stylename, showNum: false });
    $("pre.csharp" + "Code").snippet("csharp", { style: stylename, showNum: false });
    $("pre.css" + "Code").snippet("css", { style: stylename, showNum: false });
    $("pre.flex" + "Code").snippet("flex", { style: stylename, showNum: false });
    $("pre.html" + "Code").snippet("html", { style: stylename, showNum: false });
    $("pre.java" + "Code").snippet("java", { style: stylename, showNum: false });
    $("pre.javascript" + "Code").snippet("javascript", { style: stylename, showNum: false });
    $("pre.javascript_dom" + "Code").snippet("javascript_dom", { style: stylename, showNum: false });
    $("pre.perl" + "Code").snippet("perl", { style: stylename, showNum: false });
    $("pre.php" + "Code").snippet("php", { style: stylename, showNum: false });
    $("pre.python" + "Code").snippet("python", { style: stylename, showNum: false });
    $("pre.ruby" + "Code").snippet("ruby", { style: stylename, showNum: false });
    $("pre.sql" + "Code").snippet("sql", { style: stylename, showNum: false });
    $("pre.xml" + "Code").snippet("xml", { style: stylename, showNum: false });
}

代码片段高亮库提供 33 种主题供选择。

编辑片段

此页面允许已登录用户编辑现有代码片段(如果它是您的)。它首先找到您要编辑的片段。

然后,在您点击编辑图标后,您将被重定向到编辑片段页面,如下所示:

编辑页面与添加页面的工作方式非常相似,不同之处在于,其中一些字段现在是只读的。既然我们刚刚讨论了添加片段页面的工作方式,我将把编辑页面的工作方式留给您的想象力,我相信你们都是聪明人,一定能想象出来。

删除片段

CodeStash 的全部理念是它是一个片段门户,您可以管理您的片段存储库。因此,您显然可以选择删除片段(如果它是您的),这始于找到您要删除的片段。一旦您找到一个片段,您可以使用删除图标,它会显示一个弹出窗口,要求您确认删除。请记住,代码片段可以分组(您知道 C#/ASPX/HTML 都属于一个逻辑组),因此,如果当前片段在一个组中,删除对话框可能会要求您删除该组中的所有片段。或者,如果没有分组,您将看到一个标准确认对话框。

如果片段在一个组中,则显示如下:

如果片段不在组中,则显示如下:

那么这一切在幕后是如何运作的呢?实际上,它出奇地简单,如果我们回顾一下模式:

很明显,每个 CodeSnippet 都可以属于一个组,尽管这不是强制性的。因此,在此基础上,问题实际上只是将片段属于一个组的事实传递到视图中(我使用隐藏字段来完成此操作),然后让 jQuery 检查是否存在组,从而显示正确的对话框。

@if (Model.CodeSnippet.Grouping != null)
{
    <input class="isGrouped" type="hidden" 
	value="true" />
    <input class="groupId" type="hidden" 
	value="@Model.CodeSnippet.GroupId" />
    <input class="groupDescription" type="hidden" 
	value="@Model.CodeSnippet.Grouping.Description" />
        
}
else
{
    <input class="isGrouped" type="hidden" value="false" />
}

以下是相关的 jQuery

$(".delete.enabled img").click(function () {

    var displayMode = $('#DisplayMode').val();
    var snippetElement = $(this).closest(".highlightedSnippetHeader").next(".highlightedSnippet");
    var isGrouped = snippetElement.find(".isGrouped").val();
    var groupDescription = snippetElement.find(".groupDescription").val();
    var groupId = snippetElement.find(".groupId").val();
    var codeSnippetId = snippetElement.find(".codeSnippetId").val();

       

    if (isGrouped == "true") {

        showYesNoGroupSnippetDialog("Code snippet " + codeSnippetId + 
		" is part of group '" + groupDescription +
            "'.<br/><br/> Please pick whether to delete just the single selected snippet or ALL snippets in the group",
            220, "Confirm Delete",
            function (data) {
                DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, false);
            },
            function (data) {
                DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, true);
            }
        );
    }
    else {
        showYesNoDialog('Are you sure you want to delete this snippet?', 180, 'Confirm',
            function () {
                DeleteSnippetAndShowRemaining(displayMode, codeSnippetId, groupId, false);
            }
        );
    }
});

其中调用了以下控制器代码来执行实际的删除:

[Authorize]
[HttpPost]
[AjaxOnly]
public JsonResult DeleteSnippetAndShowRemaining(DisplayMode displayMode, 
    int snippetId, int groupId, bool deleteAllInGroup)
{
    ....
    ....
}

 

只读显示片段

这与非只读片段的工作方式大体相同,不同之处在于它使用了一个最小的主页面,没有任何额外的浮华,它是一个只显示片段的简单视图,您不能再编辑/删除片段,因为这些功能要求您登录到 CodeStash

插件 REST API

Pete O'Hanlon 编写的 CodeStash 插件显然需要从某个地方获取数据,也需要一个地方来存储数据。那显然是集中式的(目前,根据使用情况可能会改为云托管/非 SQL)SQL Server 数据库。

这里有几种不同的选项,如下所示,但我们想要一个开放的架构,可以支持任何类型的客户端,而不仅仅是 Windows 客户端。

  1. WCF:感觉太严格,而且是仅限 Windows。
  2. Web WCF Api:好吧,稍微好一点,但仍然意味着服务器必须与网站不同地编写。
  3. 支持 JSON CRUD 的标准控制器。这很好,因为它完全是标准的 JSON,并且完全基于 POST/GET,没有任何魔法。这就是我们选择的。

现在我们知道有一个专门用于 CodeStash 插件使用的 REST API 控制器,它看起来像什么呢?嗯,如果只考虑它的方法,它就非常简单了。

让我们看看这些。

namespace CodeStash.Controllers
{
    public class RestController : Controller
    {

        #region Ctor

        public RestController(IGetUserForRestService getUserForRestService,
                                ILoggerService loggerService,
                                IRepository<CodeSnippet> codeSnippetRepository,
                                IRepository<CodeCategory> codeCategoryRepository,
                                IRepository<CodeTag> codeTagRepository,
                                IRepository<Grouping> groupingRepository,
                                IRepository<Language> languageRepository,
                                IRepository<CreatedTeam> createdTeamRepository,
                                IRepository<Visibility> visibilityRepository,
                                IUnitOfWork unitOfWork)
        {
	    ....
	    ....

        }
        #endregion

        #region REST API
        /// <summary>
        /// Searches all available <c>CodeSnippet</c>(s) and returns a 
	    /// List<c>JSONPagesSearchResultCodeSnippet</c>(s)
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONSearchInput))]
        public ActionResult Search(JSONSearchInput input)
        {
	    ....
	    ....

        }



        /// <summary>
        /// Adds a new <c>CodeSnippet</c> and returns a 
	    /// <c>JSONCodeSnippetAddSingleResult</c>
        /// with the newly added <c>CodeSnippet</c> or an Exception that may have occurred
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONAddSnippetInput))]
        public ActionResult AddSnippet(JSONAddSnippetInput input)
        {
	    ....
	    ....

        }


        /// <summary>
        /// Gets all <c>Language</c>(s) which are returned in a 
	    ///  <c>JSONLanguages</c>
        /// with all the <c>Language</c>(s) or an Exception that may have occurred
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONCredentialInput))]
        public ActionResult GetAllLanguages(JSONCredentialInput input)
        {
	    ....
	    ....
        }


        /// <summary>
        /// Gets all <c>Grouping</c>(s) which are returned in a 
	    /// <c>JSONGrouping</c>
        /// with all the <c>Grouping</c>(s) or an Exception that may have occurred
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONCredentialInput))]
        public ActionResult GetAllGroups(JSONCredentialInput input)
        {
	    ....
	    ....
		
        }
        #endregion


    }
}

这实际上就是全部了,显然这些方法中正在进行 CRUD 操作,但这在很大程度上是无关紧要的。这里有两点需要注意。一是所有 REST API 调用都必须提供 JSONCredentialInput,这意味着提供正确格式化并经过验证的 JSONCredentialInput 的调用将能够使用 REST API。第二点是,它是一种标准的 MVC 方法,用于公开 JSON 数据,并使用模型绑定接受新的 JSON 数据。

接受模型绑定的 JSON 数据是通过专用 ActionFilter JSONInputAttribute 实现的,它看起来像这样:

public class JSONInputAttribute : ActionFilterAttribute
{
    public string Param { get; set; }

    public Type RootType { get; set; }

    private Encoding GetEncoding(string contentType)
    {
        Encoding encoding = Encoding.Default;

        switch (contentType)
        {
            case "application/json; charset=UTF-7":
                encoding= Encoding.UTF7;
                break;
            case "application/json; charset=UTF-8":
                encoding= Encoding.UTF8;
                break;
            case "application/json; charset=unicode":
                encoding= Encoding.Unicode;
                break;
            case "application/json; charset=ascii":
                encoding= Encoding.ASCII;
                break;
        }

        return encoding;
    }


    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        try
        {
            string json = filterContext.HttpContext.Request.Form[Param];

            if (json == "[]" || json == "\",\"" || String.IsNullOrEmpty(json))
            {
                filterContext.ActionParameters[Param] = null;
            }
            else
            {
                Encoding encoding = GetEncoding(filterContext.HttpContext.Request.ContentType);

                using (var ms = new MemoryStream(encoding.GetBytes(json)))
                {
                    filterContext.ActionParameters[Param] =
			new DataContractJsonSerializer(RootType).ReadObject(ms);
                }
            }
        }
        catch
        {
            filterContext.ActionParameters[Param] = null;
        }
    }

}

可以看出,这利用了 DataContractJsonSerializer,这是因为 .NET 客户端使用了相同的序列化过程。然而,这无关紧要,这个 ActionFilter 完全能够接受来自任何地方的 JSON,它只是使用 DataContractJsonSerializer 将 JSON 数据水合回 .NET 对象供控制器使用。让我们看看插件使用的这些 JSON DTO 对象之一:

[DataContract]
public partial class JSONLanguage
{

    public JSONLanguage(int languageId, string language)
    {
        this.LanguageId = languageId;
        this.Language = language;
    }

    [DataMember]
    public int LanguageId { get; set; }

    [DataMember]
    public string Language { get; set; }
}

以下是插件如何调用 REST API 的一个偷窥示例。其余部分(原谅双关语)Pete 将在他的关于插件的文章中更详细地介绍。

/// <summary>
/// Retrieve the languages from the service.
/// </summary>
public JSONLanguagesResult RetrieveLanguages()
{
    return GetValue<JSONLanguagesResult>("GetAllLanguages");
}


private T GetValue<T>(string restService) where T : class
{
    return Utilities.GetValue<T>(GetDataFromRestService(restService));
}



protected byte[] GetDataFromRestService(string restMethod)
{
    JSONCredentialInput input = new JSONCredentialInput(
        openId, // If not specified, will be an empty string but the password must be set.
        emailAddress, // Must always be present.
        password // If not specified, will be an empty string, but the OpenID must be set.
        );
    return CallService(input, CodeStash.Common.Helpers.ConfigurationSettings.RestAddress, restMethod);
}


private byte[] CallService<T>(T input, string address, string methodToCall)
{
    values = new NameValueCollection();
    Utilities.AddValue(values, "input", input);

    WebClient client = new WebClient();
    return client.UploadValues(string.Format("{0}{1}", address, methodToCall), values);
}

其中以下实用程序代码负责序列化/反序列化为 JSON:

using System;
using System.Linq;
using System.Text;
using CodeStash.Common.Encryption;
using System.Runtime.Serialization.Json;
using System.IO;
using System.Collections.Specialized;
using System.Collections.Generic;

namespace CodeStash.Addin.Core
{
    public static class Utilities
    {
        internal static DataContractJsonSerializer jss;

        public static string GetStringForWebsiteCall(this string value)
        {
            if (string.IsNullOrWhiteSpace(value))
                return string.Empty;

            if (EncryptionHelper.EncryptionEnabled)
                return EncryptionHelper.GetEncryptedValue(value);
            return value;
        }

        internal static T GetValue<T>(Byte[] results) where T : class
        {
            using (MemoryStream ms = new MemoryStream(results))
            {
                jss = new DataContractJsonSerializer(typeof(T));
                return (T)jss.ReadObject(ms);
            }
        }


        internal static void AddValue(NameValueCollection values, string key, object value)
        {
            jss = new DataContractJsonSerializer(value.GetType());
            using (MemoryStream ms = new MemoryStream())
            {
                jss.WriteObject(ms, value);
                string json = Encoding.UTF8.GetString(ms.ToArray());
                values.Add(key, json);
            }
        }
    }
}

就这些

无论如何,这就是我们的成果,我希望大家喜欢 Pete 和我共同完成的工作,我们都为此投入了大量时间,并且我们都相信它可能是一个非常有用的工具。正如我们所说,我们非常乐意听取您对此的意见,您认为它有用吗?您会使用它吗?我们可以做出哪些改进?听到大家对我们的想法真的会很好,我们试图以我们认为最适合开发人员的方式来制作它,但我们可能遗漏了一些东西,所以我们有点依赖您来告诉我们,因此,如果您希望我们在 V2 中做些什么,请不要犹豫。我们为 V2 准备了一些东西,但我们想先了解对这个首个版本的普遍感受,然后再为这个项目付出更多的汗水和努力。

© . All rights reserved.