LINQ to Entities case sensitive comparison

Sun, 15 Jan 2023 03:17:14 GMT

That's because you are using LINQ To Entities which is ultimately convert your Lambda expressions into SQL statements. That means the case sensitivity is at the mercy of your SQL Server which by default has SQL_Latin1_General_CP1_CI_AS Collation and that is NOT case sensitive.

Using ObjectQuery.ToTraceString to see the generated SQL query that has been actually submitted to SQL Server reveals the mystery:

string sqlQuery = ((ObjectQuery)context.Thingies
        .Where(t => t.Name == "ThingamaBob")).ToTraceString();

When you create a LINQ to Entities query, LINQ to Entities leverages the LINQ parser to begin processing the query and converts it into a LINQ expression tree. The LINQ expression tree is then passed to Object Services API, which converts the expression tree to a command tree. It is then sent to the store provider (e.g. SqlClient), which convert the command tree into the native database command text. Query get executed on the data store and the results are Materialized into Entity Objects by Object Services. No logic has been put in between to take case sensitivity into account. So no matter what case you put in your predicate, it will always treat as the same by your SQL Server unless you change your SQL Server Collates for that column.

Server side solution:

Therefore, the best solution would be to change the collation of the Name column in the Thingies table to COLLATE Latin1_General_CS_AS which is case sensitive by running this on your SQL Server:

ALTER TABLE Thingies
ALTER COLUMN Name VARCHAR(25)
COLLATE Latin1_General_CS_AS

For more information on the SQL Server Collates, take a a look at SQL SERVER Collate Case Sensitive SQL Query Search

Client-side solution:

The only solution that you can apply on client side is to use LINQ to Objects to do yet another comparison which doesn't seem to be very elegant:

Thingies.Where(t => t.Name == "ThingamaBob")
        .AsEnumerable()
        .First(t => t.Name == "ThingamaBob");

 

You can add [CaseSensitive] annotation for EF6+ Code-first

Add this classes

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class CaseSensitiveAttribute : Attribute
{
    public CaseSensitiveAttribute()
    {
        IsEnabled = true;
    }
    public bool IsEnabled { get; set; }
}

public class CustomSqlServerMigrationSqlGenerator : SqlServerMigrationSqlGenerator
{
    protected override void Generate(AlterColumnOperation alterColumnOperation)
    {
        base.Generate(alterColumnOperation);
        AnnotationValues values;
        if (alterColumnOperation.Column.Annotations.TryGetValue("CaseSensitive", out values))
        {
            if (values.NewValue != null && values.NewValue.ToString() == "True")
            {
                using (var writer = Writer())
                {
                    //if (System.Diagnostics.Debugger.IsAttached == false) System.Diagnostics.Debugger.Launch();

                    // https://github.com/mono/entityframework/blob/master/src/EntityFramework.SqlServer/SqlServerMigrationSqlGenerator.cs
                    var columnSQL = BuildColumnType(alterColumnOperation.Column); //[nvarchar](100)
                    writer.WriteLine(
                        "ALTER TABLE {0} ALTER COLUMN {1} {2} COLLATE SQL_Latin1_General_CP1_CS_AS {3}",
                        alterColumnOperation.Table,
                        alterColumnOperation.Column.Name,
                        columnSQL,
                        alterColumnOperation.Column.IsNullable.HasValue == false || alterColumnOperation.Column.IsNullable.Value == true ? " NULL" : "NOT NULL" //todo not tested for DefaultValue
                        );
                    Statement(writer);
                }
            }
        }
    }
}

public class CustomApplicationDbConfiguration : DbConfiguration
{
    public CustomApplicationDbConfiguration()
    {
        SetMigrationSqlGenerator(
            SqlProviderServices.ProviderInvariantName,
            () => new CustomSqlServerMigrationSqlGenerator());
    }
}

Modify your DbContext, add

protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Add(new AttributeToColumnAnnotationConvention<CaseSensitiveAttribute, bool>(
                "CaseSensitive",
                (property, attributes) => attributes.Single().IsEnabled));
        base.OnModelCreating(modelBuilder);
    }

Then do

Add-Migration CaseSensitive

Update-Database

based on article https://milinaudara.wordpress.com/2015/02/04/case-sensitive-search-using-entity-framework-with-custom-annotation/ with some bug fix

 

 

WHERE conditions in SQL Server are case insensitive by default. Make it case sensitive by changing the column's default collations (SQL_Latin1_General_CP1_CI_AS) to SQL_Latin1_General_CP1_CS_AS.

The fragile way to do this is with code. Add a new migration file and then add this inside the Up method:

public override void Up()
{
   Sql("ALTER TABLE Thingies ALTER COLUMN Name VARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL");
}

But

You can create custom annotation called "CaseSensitive" using the new EF6 features and you can decorate your properties like this:

[CaseSensitive]
public string Name { get; set; }

This blog post explains how to do that.

 

Use string.Equals

Thingies.First(t => string.Equals(t.Name, "ThingamaBob", StringComparison.CurrentCulture);

Also, you don't have to worry about null and get back only the information you want.

Use StringComparision.CurrentCultureIgnoreCase for Case Insensitive.

Thingies.First(t => string.Equals(t.Name, "ThingamaBob", StringComparison.CurrentCultureIgnoreCase);
Share
Improve this answer

 

...
浏览 : 1166 阅读全文
1.第一步
truncate table __MigrationHistory
 
2.第二步
删除HKElectric.DocMS.Migration 项目下的Migrations目录中的202209200734403_InitDatabase、202210260610213_UpdateDataBase
 
3.第三步
Add-Migration InitialCreate -IgnoreChanges
 
4.第四步
update-database
 
5.补充Sql Server 所需要的字段,如RowVersion
 
以下可以不用
6.第六步
Update-Database -Script
...
浏览 : 836 阅读全文

.net EF 6.0 Code First 迁移

Sun, 30 Oct 2022 00:23:12 GMT

如果使用的是 Code First 工作流,推荐使用 Code First 迁移改进应用程序的数据库架构。 迁移提供一组允许以下操作的工具:

  1. 创建可用于 EF 模型的初始数据库
  2. 生成迁移以跟踪对 EF 模型所做的更改
  3. 使数据库随时掌握这些更改

下方演练将概述实体框架中的 Code First 迁移。 可以完成整个演练或跳到感兴趣的主题。 包含以下主题:

生成初始模型 & 数据库

开始使用迁移之前,需要会用到项目和 Code First 模型。 对于此演练,我们将使用规范的“博客”和“帖子”模型 。

  • 创建新的 MigrationsDemo 控制台应用程序
  • 将最新版本的 EntityFramework NuGet 包添加到项目中
    • “工具”–>“库包管理器”–>“包管理器控制台”
    • 运行 Install-Package EntityFramework 命令
  • 添加 Model.cs 文件,其代码如下所示 。 此代码定义了构成域模型的单个“博客”类和 EF Code First 上下文 BlogContext 类
C#
    using System.Data.Entity;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Data.Entity.Infrastructure;

    namespace MigrationsDemo
    {
        public class BlogContext : DbContext
        {
            public DbSet<Blog> Blogs { get; set; }
        }

        public class Blog
        {
            public int BlogId { get; set; }
            public string Name { get; set; }
        }
    }
  • 现在我们拥有一个模型,可用它执行数据访问操作。 更新 Program.cs 文件,其代码如下所示 。
C#
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    namespace MigrationsDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                using (var db = new BlogContext())
                {
                    db.Blogs.Add(new Blog { Name = "Another Blog " });
                    db.SaveChanges();

                    foreach (var blog in db.Blogs)
                    {
                        Console.WriteLine(blog.Name);
                    }
                }

                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }
        }
    }
  • 运行应用程序,随即会创建 MigrationsCodeDemo.BlogContext 数据库 。

    数据库 LocalDB

启用迁移

现可对模型进行更多更改。

  • 将 Url 属性引入“博客”类。
C#
    public string Url { get; set; }

如果要再次运行应用程序,则会收到一个 InvalidOperationException,指出“创建数据库后,支持‘BlogContext’上下文的模型已发生变化。考虑使用 Code First 迁移来更新数据库(http://go.microsoft.com/fwlink/?LinkId=238269)。

如异常情况所述,可开始使用 Code First 迁移。 第一步是启用上下文迁移。

  • 在包管理器控制台中运行 Enable-Migrations 命令

    此命令已将“迁移”文件夹添加到项目中 。 此新文件夹包含两个文件:

  • 配置类。 此类允许配置迁移对上下文的行为方式。 对于此演练,将只使用默认配置。 由于项目中只有一个 Code First 上下文,因此 Enable-Migrations 已自动填充此配置适用的上下文类型。

  • InitialCreate 迁移。 之所以生成此迁移,是因为在启用迁移之前,我们已使用 Code First 创建了数据库。 已构建的迁移中的代码表示已在数据库中创建的对象。 在本演练中,即为具有 BlogId 和“名称”列的 Blog 表 。 文件名包含时间戳,这样有助于排序。 如果尚未创建数据库,则不会将此 InitialCreate 迁移添加到项目中。 相反,第一次调用 Add-Migration 时,会将创建这些表的代码构建到新的迁移中。

针对同一数据库的多个模型

使用 EF6 之前的版本时,只能使用一个 Code First 模型生成/管理数据库的架构。 这是因为每个数据库的单个 __MigrationsHistory 表无法识别哪些项属于哪个模型。

从 EF6 开始,配置类中包括 ContextKey 属性 。 该属性充当每个 Code First 模型的唯一标识符。 __MigrationsHistory 表中相应的列允许来自多个模型的项共享表。 默认情况下,此属性设置为上下文的完全限定名称。

生成 & 运行迁移

Code First 迁移具有两个需要用户了解的主要命令。

  • Add-Migration 将基于自上次迁移创建以来对模型所做的更改来构建下一次迁移
  • Update-Database 将对数据库应用任意挂起的迁移

我们需要构建迁移来处理添加的新 Url 属性。 Add-Migration 命令可为这些迁移命名,仅需调用 AddBlogUrl 。

  • 在包管理器控制台中运行 Add-Migration AddBlogUrl 命令
  • 现“迁移”文件夹中具有新的 AddBlogUrl 迁移 。 迁移文件名以时间戳作为前缀,这样有助于排序
C#
    namespace MigrationsDemo.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;

        public partial class AddBlogUrl : DbMigration
        {
            public override void Up()
            {
                AddColumn("dbo.Blogs", "Url", c => c.String());
            }

            public override void Down()
            {
                DropColumn("dbo.Blogs", "Url");
            }
        }
    }

现可编辑或添加到此迁移,但所有内容看起来都很合适。 使用 Update-Database 将此迁移应用到数据库 。

  • 在包管理器控制台运行 Update-Database 命令
  • Code First 迁移将比较“迁移”文件夹中的迁移和已应用于数据库的迁移 。 迁移会发现需应用 AddBlogUrl 迁移,并运行它 。

MigrationsDemo.BlogContext 数据库现已更新,其中包含“博客”表中的 Url 列 。

自定义迁移

到目前为止,我们已在未进行任何更改的情况下生成并运行了迁移。 现在我们来看看如何编辑默认生成的代码。

  • 现在可对模型进行更多更改,我们将新的 Rating 属性添加到“博客”类
C#
    public int Rating { get; set; }
  • 同时添加一个新的“帖子”类
C#
    public class Post
    {
        public int PostId { get; set; }
        [MaxLength(200)]
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
  • 此外,再将“帖子”集合添加到“博客”类,以形成“博客”和“帖子”之间的另一层关系
C#
    public virtual List<Post> Posts { get; set; }

使用 Add-Migration 命令使 Code First 迁移提供对迁移的最佳猜测 我们将调用此迁移 AddPostClass 。

  • 在包管理器控制台中运行 Add-Migration AddPostClass 命令 。

Code First 迁移出色的构建了这些更改,但我们可能还需要做出一些更改:

  1. 首先,将唯一索引添加到 Posts.Title 列(添加在以下代码的 22 和 29 行)。
  2. 同时添加不可为 NULL 的 Blogs.Rating 列 。 如果表中存在任何现有数据,则会为数据分配新列数据类型的 CLR 默认值(分级是整数,因此将为 0) 。 但我们要将默认值指定为 3,以便“博客”表中的现有行以合适的分级开始 。 (可以看到以下代码的第 24 行指定的默认值)
C#
    namespace MigrationsDemo.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;

        public partial class AddPostClass : DbMigration
        {
            public override void Up()
            {
                CreateTable(
                    "dbo.Posts",
                    c => new
                        {
                            PostId = c.Int(nullable: false, identity: true),
                            Title = c.String(maxLength: 200),
                            Content = c.String(),
                            BlogId = c.Int(nullable: false),
                        })
                    .PrimaryKey(t => t.PostId)
                    .ForeignKey("dbo.Blogs", t => t.BlogId, cascadeDelete: true)
                    .Index(t => t.BlogId)
                    .Index(p => p.Title, unique: true);

                AddColumn("dbo.Blogs", "Rating", c => c.Int(nullable: false, defaultValue: 3));
            }

            public override void Down()
            {
                DropIndex("dbo.Posts", new[] { "Title" });
                DropIndex("dbo.Posts", new[] { "BlogId" });
                DropForeignKey("dbo.Posts", "BlogId", "dbo.Blogs");
                DropColumn("dbo.Blogs", "Rating");
                DropTable("dbo.Posts");
            }
        }
    }

已编辑的迁移准备就绪,所以我们使用 Update-Database 来更新数据库 。 这次指定 –Verbose 标志,以便可以看到 Code First 迁移正在运行的 SQL 。

  • 在包管理器控制台中运行 Update-Database –Verbose 命令 。

数据移动/自定义 SQL

到目前为止,我们已经介绍了不更改或移动任何数据的迁移操作,现在来看看需要移动数据的操作。 目前还没有对数据移动的原生支持,但我们可以在脚本中的任何位置运行一些任意 SQL 命令。

  • 将 Post.Abstract 属性添加到模型中 。 稍后,我们将使用“内容”列开头的一些文本预填充现有帖子的“摘要” 。
C#
    public string Abstract { get; set; }

使用 Add-Migration 命令使 Code First 迁移提供对迁移的最佳猜测

  • 在包管理器控制台中运行 Add-Migration AddPostAbstract 命令 。
  • 生成的迁移会处理架构更改,但我们还是希望使用每个帖子内容的前 100 个字符预填充“摘要”列 。 要执行此操作,可在添加列之后下拉到 SQL 并运行 UPDATE 语句 。 (添加在以下代码的第 12 行中)
C#
    namespace MigrationsDemo.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;

        public partial class AddPostAbstract : DbMigration
        {
            public override void Up()
            {
                AddColumn("dbo.Posts", "Abstract", c => c.String());

                Sql("UPDATE dbo.Posts SET Abstract = LEFT(Content, 100) WHERE Abstract IS NULL");
            }

            public override void Down()
            {
                DropColumn("dbo.Posts", "Abstract");
            }
        }
    }

已编辑的迁移一切正常,所以我们可使用 Update-Database 来更新数据库 。 我们将指定 –Verbose 标志,以便可以看到针对数据库运行的 SQL 。

  • 在包管理器控制台中运行 Update-Database –Verbose 命令 。

迁移到特定版本(包括降级)

到目前为止,我们一直在升级到最新迁移,但用户有时可能希望升级/降级到特定迁移。

假设想在运行 AddBlogUrl 迁移后将数据库迁移到其之前的状态 。 此时可使用 –TargetMigration 切换为降级到此迁移 。

  • 在包管理器控制台中运行 Update-Database –TargetMigration: AddBlogUrl 命令。

此命令将为 AddBlogAbstract 和 AddPostClass 迁移运行 Down 脚本 。

如果想要一直回退到空数据库,可使用 Update-Database –TargetMigration: $InitialDatabase 命令 。

获取 SQL 脚本

如果另一位开发人员希望在其计算机上进行这些更改,则只需在我们将更改签入源代码管理之后进行同步即可。 在获得我们的新迁移后,他们只需运行 Update-database 命令即可在本地应用更改。 但是,如果想将这些更改推送到测试服务器以及最终的产品,则可能需要一个可以传递给 DBA 的 SQL 脚本。

  • 运行 Update-Database 命令,但是这次需指定 –Script 标志,以便将更改写入脚本,而不是应用更改 。 我们还将指定要为其生成脚本的源和目标迁移。 我们希望脚本从空数据库 ($InitialDatabase) 转为最新版本(迁移 AddPostAbstract) 。 如果未指定目标迁移,迁移将使用最新的迁移作为目标。 如果未指定源迁移,迁移将使用数据库的当前状态。
  • 在包管理器控制台中运行 Update-Database -Script -SourceMigration: $InitialDatabase -TargetMigration: AddPostAbstract 命令

Code First 迁移将运行迁移管道,但并非是应用更改,而是将更改写入到 .sql 文件。 生成脚本后,将在 Visual Studio 中打开,以供查看或保存。

生成幂等脚本

从 EF6 开始,如果指定 –SourceMigration $InitialDatabase,则生成的脚本将为“幂等” 。 幂等脚本可以将当前任何版本的数据库升级到最新版本(或使用 – TargetMigration 升级到指定版本) 。 生成的脚本包括检查 __MigrationsHistory 表的逻辑,并且仅应用以前未应用的更改。

应用程序启动时自动升级(MigrateDatabaseToLatestVersion 初始值设定项)

如果要部署应用程序,则可能希望应用程序在启动时自动升级数据库(通过应用各种挂起的迁移)。 可以通过注册 MigrateDatabaseToLatestVersion 数据库初始值设定项来执行此操作 。 数据库初始值设定项仅包含一些用于确保正确设置数据库的逻辑。 第一次在应用程序进程中使用上下文时,会运行此逻辑 (AppDomain) 。

如下所示,可以更新 Program.cs 文件,以在使用上下文(第 14 行)之前,为 BlogContext 设置 MigrateDatabaseToLatestVersion 初始化值设定项 。 请注意,还需要为 System.Data.Entity 命名空间(第 5 行)添加 using 语句 。

创建此初始值设定项的实例时,需要指定上下文类型 (BlogContext) 和迁移配置(配置)- 迁移配置是启用迁移时添加到“迁移”文件夹的类

C#
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Data.Entity;
    using MigrationsDemo.Migrations;

    namespace MigrationsDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                Database.SetInitializer(new MigrateDatabaseToLatestVersion<BlogContext, Configuration>());

                using (var db = new BlogContext())
                {
                    db.Blogs.Add(new Blog { Name = "Another Blog " });
                    db.SaveChanges();

                    foreach (var blog in db.Blogs)
                    {
                        Console.WriteLine(blog.Name);
                    }
                }

                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }
        }
    }

现在,每当应用程序运行时,它首先会检查其目标数据库是否为最新,如果不是,则会应用各种挂起的迁移。

 

 

...
浏览 : 1128 阅读全文

EF Migration操作

Sat, 29 Oct 2022 17:47:16 GMT
1. 卸载EF
Uninstall-Package EntityFramework –Force
2. 安装EF
Install-Package EntityFramework
3.启动迁移
Enable-Migrations
4.添加迁移
 Add-Migration InitialCreate -IgnoreChanges
 Add-Migration add_UserInfo_Status
5. 修改生成的Migration文件
    如我给表增加了一个字段
        public override void Up()
        {
            AddColumn("UserInfo", "Status", c => c.Int());
        }
6. 更新到数据库
update-database -force7.用指定的迁移类更新数据库update-database -TargetMigration:MigrationClassName
...
浏览 : 896 阅读全文
复制代码
<configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.4.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
    <!-- Log4Net配置声明 -->
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
  </configSections>
  <!-- Log4Net具体配置 -->
  <log4net>
    <!-- OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL -->
    <!-- Set root logger level to ERROR and its appenders -->
    <root>
      <level value="ALL"/>
      <appender-ref ref="SysAppender"/>
    </root>
    <!-- Print only messages of level DEBUG or above in the packages -->
    <logger name="WebLogger">
      <level value="DEBUG"/>
    </logger>
    <appender name="SysAppender" type="log4net.Appender.RollingFileAppender,log4net" >
      <param name="File" value="App_Data/" />
      <param name="AppendToFile" value="true" />
      <param name="RollingStyle" value="Date" />
      <param name="DatePattern" value="&quot;Logs_&quot;yyyyMMdd&quot;.txt&quot;" />
      <param name="StaticLogFileName" value="false" />
      <layout type="log4net.Layout.PatternLayout,log4net">
        <!--<param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />-->
        <param name="ConversionPattern" value="记录时间:%date %n线程ID: [%thread] %n日志级别:%-5level %n出错类:%logger property: [%property{NDC}] - %n错误描述:%message%newline %n" />
        <param name="Header" value="-------------------------------------------------------header-----------------------------------------------------------&#13;&#10;" />
        <param name="Footer" value="-------------------------------------------------------footer-----------------------------------------------------------&#13;&#10;" />
      </layout>
    </appender>
    <appender name="consoleApp" type="log4net.Appender.ConsoleAppender,log4net">
      <layout type="log4net.Layout.PatternLayout,log4net">
        <param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
      </layout>
    </appender>
  </log4net>
复制代码

PS:Log4Net是用来记录日志的一个常用组件(Log4J的移植版本),可以将程序运行过程中的信息输出到一些地方(文件、数据库、EventLog等)。由于Log4Net不是本篇博文介绍的重点,所以对Log4Net不熟悉的朋友,请在博客园首页搜索:Log4Net,浏览其详细的介绍。

  其次,在App_Start文件夹中添加一个类,取名为LogConfig,定义一个静态方法:RegisterLog4NetConfigure,具体代码只有一行,实现了Log4Net配置的初始化操作。

复制代码
    public class LogConfig
    {
        public static void RegisterLog4NetConfigure()
        {
            //获取Log4Net配置信息(配置信息定义在Web.config文件中)
            log4net.Config.XmlConfigurator.Configure();
        }
    }
复制代码

  最后,在Global.asax中的Application_Start方法中添加一行代码,注册Log4Net的配置:

复制代码
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);


            //自定义事件注册
            MessageQueueConfig.RegisterExceptionLogQueue();
            LogConfig.RegisterLog4NetConfigure();
        }
复制代码

  (5)第五步,改写MessageQueueConfig中的RegisterExceptionLogQueue方法。这里就不再需要从预置类型Queue中取任务了,而是Redis中取出任务出队进行相应处理。这里,我们使用了Log4Net进行异常日志的记录工作。PS:注意在代码顶部添加对log4net的引用:using log4net;

复制代码
      public static void RegisterExceptionLogQueue()
        {
            //通过线程池开启线程,不停地从队列中获取异常信息并将其写入日志文件
            ThreadPool.QueueUserWorkItem(o =>
            {
                while (true)
                {
                    try
                    {
                        if (MyExceptionFilterAttribute.redisClient.GetListCount("ExceptionLog") > 0)
                        {
                            //从队列中出队,获取异常对象
                            string errorMsg = MyExceptionFilterAttribute.redisClient.DequeueItemFromList("ExceptionLog");
                            if (!string.IsNullOrEmpty(errorMsg))
                            {
                                //使用Log4Net写入异常日志
                                ILog logger = LogManager.GetLogger("Log");
                                logger.Error(errorMsg);
                            }
                        }
                        else
                        {
                            Thread.Sleep(1000); //为避免CPU空转,在队列为空时休息1秒
                        }
                    }
                    catch (Exception ex)
                    {
                        MyExceptionFilterAttribute.redisClient.EnqueueItemOnList("ExceptionLog", ex.ToString());
                    }
                }
            });
        }
复制代码

   (6)最后一步,调试验证是否能正常写入App_Data文件的日志中,发现写入的异常日志如下,格式好看,信息详细,圆满完成了我们的目的。

四、小结

  使用消息队列将调用异步化,可以改善网站系统的性能:消息队列具有很好的削峰作用,即通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。在电商网站的促销活动中,合理使用消息队列,可以有效地抵御促销活动刚开始大量涌入的订单对系统造成的冲击。本文使用消息队列的思想,借助Redis+Log4Net完成了一个超简单的异常日志队列的应用案例,可以有效地解决在多线程操作中对日志文件的并发操作带来的一些问题。同样地,借助消息队列的思想,我们也可以完成对数据库的高并发的消息队列方案。所以,麻雀虽小五脏俱全,理解好了这个案例,相信对我们这些菜鸟码农是有所裨益的。同样,也请大牛们一笑而过,多多指教菜鸟们一步一步地提高,谢谢了!后边,我们会探索一下Redis的集群、主从复制,以及在VMWare中建立几台虚拟机来构建主从结构,并使用Redis记录网站中重要的Session会话对象,或者是电商项目中常见的商品类目信息等。但是,本人资质尚浅,并且都是一些初探性质的学习,如有错误和不当,还请各位园友多多指教!

参考文献

(1)传智播客.Net学院王承伟,数据优化技术之Redis公开课,http://bbs.itcast.cn/thread-26525-1-1.html

(2)Sanfilippo/贾隆译,《几点建议,让Redis在你的系统中发挥更大作用》,http://database.51cto.com/art/201107/276333.htm

(3)NoSQLFan,《Redis作者谈Redis应用场景》,http://blog.nosqlfan.com/html/2235.html

(4)善心如水,《C#中使用Log4Net记录日志》,http://www.cnblogs.com/wangsaiming/archive/2013/01/11/2856253.html

(5)逆心,《ServiceStack.Redis之IRedisClient》,http://www.cnblogs.com/kissdodog/p/3572084.html

(6)李智慧,《大型网站技术架构-核心原理与案例分析》,http://item.jd.com/11322972.html

附件下载

(1)版本1:使用预置类型的异常日志队列Demo,http://pan.baidu.com/s/1nt5G7Fj

(2)版本2:使用Redis+Log4Net的异常日志队列Demo,http://pan.baidu.com/s/1i3gMnnJ

...
浏览 : 12668 阅读全文

一、Redis API For .Net

  首先,不得不说Redis官方提供了众多的API开发包,但是目前Redis官方版本不支持.Net直接进行连接,需要使用一些第三方的开源类库。目前最流行的就是ServiceStack.Redis这个开源项目,其在GitHub上的下载地址为:https://github.com/ServiceStack/ServiceStack.Redis

  进入下载页面,点击“Download Zip”按钮,即可下载该API包。解压该Zip包后,其实我们所用到的只是其中的几个DLL而已,打开build/release/MonoDevelop文件夹,看到里边还有一个zip包,这里边就是我们所需的DLL了。

  再次解压这个Zip包,可以看到其中包含如下图所示的DLL文件,这几个也是我们今天所要引入VS的DLL库,有了它们,我们就可以在程序端和Redis服务端进行对话了,是不是很赞?

  这时,我们就可以在VS中新建一个控制台项目,命名为RedisDemo,然后新建一个Lib文件夹用来存放我们的DLL文件,然后添加对这些DLL引用。至此,就是万事俱备只欠东风了,我们接下来会在程序中调用Redis客户端和Redis服务端进行通信,了解Redis API为我们提供的丰富的数据类型。

二、Redis中常用数据类型

  由上一篇博客可知,Redis目前提供五种数据类型:string(字符串)、list(链表)、Hash(哈希)、set(集合)及zset(sorted set)  (有序集合)。现在,我们一一来看看这五种数据类型的基本使用方法。在开始介绍之前,我们先使用刚刚引入的Redis API建立一个Redis客户端对象,有了这个客户端对象,我们才能和Redis服务端进行通信,且看下面的一行代码。我们需要事先指定好Redis服务端的IP地址和端口号,然后根据这两个信息建立一个RedisClient的对象实例,通过这个实例所带的方法和服务端通信。

复制代码
using System;
using System.Collections.Generic;
using ServiceStack.Redis;

namespace RedisDemo.FirstStart
{
    class Program
    {
        //Redis服务器IP地址
        static string localHostIP = "127.0.0.1";
        //Redis服务端口号
        static int redisServicePort = 6379;           

        static void Main(string[] args)
        {
            var redisClient = new RedisClient(localHostIP, redisServicePort);

            Console.ReadKey();
        }
    }
}
复制代码

  2.1 String 字符串

  String是最常用的一种数据类型,普通的key/value存储都可以归为此类 。一个Key对应一个Value,string类型是二进制安全的。Redis的string可以包含任何数据,比如jpg图片(生成二进制)或者序列化的对象。

  在Redis中的基本操作如下所示:

复制代码
     static void StringTypeDemo(RedisClient redisClient)
        {
            //向Redis中添加一个Key/Value对
            redisClient.Set<string>("username", "edisonchou");
            //从Redis中读取一个Value值
            string userName = redisClient.Get<string>("username");

            Console.WriteLine("The value from Redis is {0}", userName);
        }
复制代码

  运行效果如下:

  2.2 Hash 哈希

  Hash是一个string 类型的field和value的映射表。Hash特别适合存储对象,相对于将对象的每个字段存成单个string 类型。一个对象存储在Hash类型中会占用更少的内存,并且可以更方便的存取整个对象。
  这里借用群叔的描述,我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有以下2种存储方式:
  第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
  第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的
  因此,基于以上两种方式的缺陷,Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口,如下图:
  也就是说,Key仍然是用户ID, value是一个Map,这个Map的key是成员的属性名,value是属性值,这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题,也就很好的解决了问题。
  下面我们在VS中来看看Hash类型如何Code:
复制代码
        static void HashTypeDemo(RedisClient redisClient)
        {
            redisClient.SetEntryInHash("user", "userinfo", "cool boy");
            redisClient.SetEntryInHash("user", "useraccount", "5000");

            List<string> keyList = redisClient.GetHashKeys("user");

            foreach (string key in keyList)
            {
                Console.WriteLine(key);
                string value = redisClient.GetValueFromHash("user", key);
                Console.WriteLine("user:{0}:{1}", key, value);
            }
        }
复制代码
  运行结果如下图:

  2.3 List 链表

  Lst是一个链表结构,主要功能是push与pop,获取一个范围的所有的值等,操作中key理解为链表名字。 Redis的List类型其实就是一个每个子元素都是string类型的双向链表,我们可以通过push或pop操作从链表的头部或者尾部添加删除元素,这样List既可以作为栈,又可以作为队列。它即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

  (1)现在我们首先来看看List作为(Stack)栈类型的使用: 

  那么在VS中如何来Code呢?通过Push与Pop操作Stack

复制代码
        static void StackTypeDemo(RedisClient redisClient)
        {
            redisClient.PushItemToList("userenname", "edisonchou");
            redisClient.PushItemToList("userenname", "wncudchou");
            redisClient.PushItemToList("userenname", "milkye");
            redisClient.PushItemToList("userenname", "dickgu");

            int length = redisClient.GetListCount("userenname");
            for (int i = 0; i < length; i++)
            {
                Console.WriteLine(redisClient.PopItemFromList("userenname"));
            }
        }
复制代码

  运行效果如下:

  (2)下面我们来看看List作为(Queue)队列的使用:

  那么在VS中如何Code呢?通过DeQueue和EnQueue操作Queue

复制代码
        static void QueueTypeDemo(RedisClient redisClient)
        {
            redisClient.EnqueueItemOnList("account", "马云");
            redisClient.EnqueueItemOnList("account", "马化腾");
            redisClient.EnqueueItemOnList("account", "李彦宏");

            int length = redisClient.GetListCount("account");
            for (int i = 0; i < length; i++)
            {
                Console.WriteLine(redisClient.DequeueItemFromList("account"));
            }
        }
复制代码

  运行效果如下:

  2.4 Set 集合

  Set是string类型的无序集合。set是通过hash table实现的,添加,删除和查找,对集合我们可以取并集,交集,差集,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。

  与List比较而言,set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

  那么在VS中我们使用Set来Code一下,先增加两个Set集合,然后对其进行交集、并集与差集运算:

复制代码
    static void SetTypeDemo(RedisClient redisClient)
        {
            redisClient.AddItemToSet("a3", "ddd");
            redisClient.AddItemToSet("a3", "ccc");
            redisClient.AddItemToSet("a3", "tttt");
            redisClient.AddItemToSet("a3", "sssh");
            redisClient.AddItemToSet("a3", "hhhh");
            redisClient.AddItemToSet("a4", "hhhh");
            redisClient.AddItemToSet("a4", "h777");

            Console.WriteLine("-------------求a3集合------------");

            HashSet<string> hashSet = redisClient.GetAllItemsFromSet("a3");
            foreach (string value in hashSet)
            {
                Console.WriteLine(value);
            }

            Console.WriteLine("-------------求并集------------");

            hashSet.Clear();
            hashSet = redisClient.GetUnionFromSets(new string[] { "a3", "a4" });
            foreach (string value in hashSet)
            {
                Console.WriteLine(value);
            }

            Console.WriteLine("-------------求交集------------");

            hashSet.Clear();
            hashSet = redisClient.GetIntersectFromSets(new string[] { "a3", "a4" });
            foreach (string value in hashSet)
            {
                Console.WriteLine(value);
            }

            Console.WriteLine("-------------求差集------------");

            hashSet.Clear();
            hashSet = redisClient.GetDifferencesFromSet("a3", new string[] { "a4" });
            foreach (string value in hashSet)
            {
                Console.WriteLine(value);
            }
        }
复制代码

  运行效果如下:

  2.5 Sorted Set 有序集合

  Sorted Set 是set的一个升级版本,又被称为ZSet,它在set的基础上增加了一个顺序的属性,这一属性在添加修改。元素的时候可以指定,每次指定后,zset(表示有序集合)会自动重新按新的值调整顺序。可以理解为有列的表,一列存 value,一列存顺序。操作中key理解为zset的名字。
  sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构此外,还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
  下面,我们在VS中编写对Sorted Set的操作代码,输出时会按字母的有序顺序输出:
复制代码
        static void SortedSetTypeDemo(RedisClient redisClient)
        {
            redisClient.AddItemToSortedSet("a5", "ffff");
            redisClient.AddItemToSortedSet("a5", "bbbb");
            redisClient.AddItemToSortedSet("a5", "gggg");
            redisClient.AddItemToSortedSet("a5", "cccc");
            redisClient.AddItemToSortedSet("a5", "waaa");

            List<string> list = redisClient.GetAllItemsFromSortedSet("a5");
            foreach (string str in list)
            {
                Console.WriteLine(str);
            }
        }
复制代码

  运行效果如下:

三、小结

  Redis为我们提供了相比Memcached更为丰富多样的数据类型,使得Redis可以用在多种多样的应用场合,这也说明了为什么Redis在国内外各大互联网名企中这么受欢迎的原因。本篇简单地介绍和使用了各种数据类型,下一篇,我们会针对1-2个具体应用场景来使用这些数据类型进行具体案例的Code。最后,还是谢谢参考资料的原作者,站在你们的肩膀上,我们才能看的更远!

参考文献

(1)传智播客公开课,王承伟主讲,http://bbs.itcast.cn/thread-26525-1-1.html

(2)群叔,《Redis数据类型详解及Redis适用场景》,http://www.cnblogs.com/qunshu/p/3196972.html

附件下载

(1)ServiceStack.Redis:http://pan.baidu.com/s/1sjtxe5v

...
浏览 : 7559 阅读全文

一、NoSQL的风生水起

 1.1 后Web2.0时代的发展要求

  随着互联网Web2.0网站的兴起,传统的关系数据库在应付Web2.0网站,特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题:

  (1)对数据库高并发读写的需求

  网站要根据用户个性化信息来实时生成动态页面和提供动态信息,所以基本上无法使用动态页面静态化技术,因此数据库并发负载非常高,往往要达到每秒上万次读写请求。关系数据库应付上万次SQL查询还勉强顶得住,但是应付上万次SQL写数据请求,硬盘IO就已经无法承受了。

  (2)对海量数据的高效率存储和访问的需求

  对于大型的SNS网站,每天用户产生海量的用户动态,以国外的Friendfeed为例,一个月就达到了2.5亿条用户动态,对于关系数据库来说,在一张2.5亿条记录的表里面进行SQL查询,效率是极其低下乃至不可忍受的。

  (3)对数据库的高可扩展性和高可用性的需求

  在基于Web的架构当中,数据库是最难进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,你的数据库却没有办法像Web服务器和应用服务器那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。对于很多需要提供7*24小时不间断服务的网站来说,对数据库系统进行升级和扩展是非常痛苦的事情,往往需要停机维护和数据迁移,为什么数据库不能通过不断的添加服务器节点来实现扩展呢?

  在上面提到的“三高”的需求面前,关系数据库遇到了难以克服的障碍,而对于Web2.0网站来说,关系数据库的很多主要特性却往往无用武之地,例如:

  (1)数据库事务一致性需求

  很多Web实时系统并不要求严格的数据库事务,对读一致性的要求很低,有些场合对写一致性要求也不高。因此数据库事务管理成了数据库高负载下一个沉重的负担。

  (2)数据库的写实时性和读实时性需求

  对关系数据库来说,插入一条数据之后立刻查询,是肯定可以读出来这条数据的。并不要求这么高的实时性。

  (3)对复杂的SQL查询,特别是多表关联查询的需求

  任何大数据量的Web系统,都非常忌讳多个大表的关联查询,以及复杂的数据分析类型的复杂SQL报表查询,特别是SNS类型的网站,从需求以及产品设计角度,就避免了这种情况的产生。往往更多的只是单表的主键查询,以及单表的简单条件分页查询,SQL的功能被极大的弱化了。

  因此,关系数据库在这些越来越多的应用场景下显得不那么合适了,为了解决这类问题的非关系数据库应运而生。NoSQL 是非关系型数据存储的广义定义。它打破了长久以来关系型数据库与ACID理论大一统的局面。NoSQL 数据存储不需要固定的表结构(例如以键值对存储,它的结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,这样就不会局限于固定的结构,可以减少一些时间和空间的开销),通常也不存在连接操作。

 1.2 NoSQL无与伦比的特点

  在大数据存取上具备关系型数据库无法比拟的性能优势例如:

  (1)易扩展

  NoSQL数据库种类繁多,但是一个共同的特点都是去掉关系数据库的关系型特性。数据之间无关系,这样就非常容易扩展。也无形之间,在架构的层面上带来了可扩展的能力。

  (2)大数据量,高性能

  NoSQL数据库都具有非常高的读写性能,尤其在大数据量下,同样表现优秀。这得益于它的无关系性,数据库的结构简单。

  (3)灵活的数据模型

  NoSQL无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式。而在关系数据库里,增删字段是一件非常麻烦的事情。如果是非常大数据量的表,增加字段简直就是一个噩梦。这点在大数据量的Web2.0时代尤其明显。

  (4)高可用

  NoSQL在不太影响性能的情况,就可以方便的实现高可用的架构。比如Cassandra,HBase模型,通过复制模型也能实现高可用。

  综上所述,NoSQL的非关系特性使其成为了后Web2.0时代的宠儿,助力大型Web2.0网站的再次起飞,是一项全新的数据库革命性运动。

二、Redis的脱颖而出

  随着应用对高性能需求的增加,NoSQL逐渐在各大名企的系统架构中生根发芽。时至今日,涌现出的NoSQL产品已经有很多种了,例如Membase、MongoDB、Apache Cassandra、CouchDB等。不过,在国内外互联网巨头例如社交巨头新浪微博、传媒巨头Viacom及图片分享领域佼佼者Pinterest等名企都不约而同地采用了Redis作为其NoSQL数据库的选择,到底Redis是何方神圣呢?能让如此多的名企为它而痴狂。

  按照官方的说法,Redis是一个开源的,使用C语言编写,面向“键/值”(Key/Value)对类型数据的分布式NoSQL数据库系统,特点是高性能持久存储适应高并发的应用场景。因此,可以说Redis纯粹为应用而产生,它是一个高性能的key-value数据库,并且还提供了多种语言的API(包括我们的大C#)。那么,也许我们会问:到底性能如何呢?以下是官方的bench-mark数据:

  测试完成了50个并发执行100000个请求
  设置和获取的值是一个256字节字符串。
  Linux box是运行Linux 2.6,这是X3320 Xeon 2.5 ghz。
  文本执行使用loopback接口(127.0.0.1)。
  结果:读的速度是110000次/s,写的速度是81000次/s 。(当然不同的服务器配置性能也有所不同)。

  和Memcached类似,Redis支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序。与Memcached一样,为了保证效率,数据都是缓存在内存中。区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步(数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。)。

  因此,Redis的出现,很大程度补偿了Memcached这类key/value存储的不足,在部分场合可以对关系数据库起到很好的补充作用

三、Redis在Windows上的安装与配置

  前面介绍了一大堆,现在开始真刀实干!首先,肯定是安装Redis,这里我们选择比较熟悉的Windows平台来进行安装。既然是Windows平台,那么肯定要选择一个Windows版本的Redis安装包(其实NoSQL大部分都是部署在Linux服务器上得,什么原因?大家都懂得,开源+免费=成熟的服务器方案)。

  (1)进入GitHub的Redis相关包下载页面:https://github.com/MSOpenTech/redis

    Of course,这个地址也是可以的:https://github.com/dmajkic/redis/downloads

  (2)选择相应版本,我这里选择的是2.6的版本,点击Download ZIP按钮进行下载;

  (3)打开压缩包,可以看到我们下载的其实是一个完整的Redis-2.6的包,包含了bin、src等文件夹,src是源码,而bin则是编译好的执行文件,也是我们主要使用的东东。进入bin目录,可以看到里面又有两个目录,一个是32位操作系统使用的,另一个则是64位操作系统使用的。

  (4)我所使用的是64位系统,所以我将redisbin64.zip拷贝出来,解压后移动至我的D:/Redis目录中,可以看到解压后的内容包含以下的一些可执行exe文件:

  下面来看看这几个可执行exe文件都是干嘛用的?

  ①redis-server.exe:服务程序,也是最最最核心的一个程序。说到底,Redis也就是我们服务器系统上一直运行的一个服务甚至就是一个进程而已。 

  ②redis-check-dump.exe:本地数据库检查

  ③redis-check-aof.exe:更新日志检查

  ④redis-benchmark.exe:性能测试,用以模拟同时由N个客户端发送M个 SETs/GETs 查询。上面所提到的测试结果就是使用的该程序来进行的。

  ⑤redis-cli.exe: 服务端开启后,我们的客户端就可以输入各种命令测试了,例如GET、SET等操作;

  另外,将刚刚下载的包里边的redis.conf默认配置文件拷贝到工作目录中(我的是D:/Redis),redis.conf主要是一些Redis的默认服务配置,包括默认端口号(一般默认6379)啊之类的配置。

  (5)既然我们知道了redis-server.exe是最核心的一个程序,那么我们就首先来将它开启。这里需要在Windows的命令行界面中来开启,首先在运行窗口输入cmd进入命令窗口,使用cd命令切换到指定的目录(我是将刚刚解压的文件放在了D:/Redis文件夹下)

  (6)最后就是惊心动魄地开启Redis的服务了,输入一句简单的命令:redis-server.exe redis.conf

  这里需要注意的是:开启Redis服务后,命令行窗口不要关闭,一旦关闭,Redis服务也会相应关闭。因此,我们一般会将其改为Windows服务,并且设置为开机自动启动,就像我们数据库服务器中的SQL Server服务和Web服务器中的IIS服务一样。

  (7)究竟我们的Redis安装好了没呢?我们可以通过新打开(记得是新打开一个,而不是将原来那个关闭了)一个cmd窗口使用redis-cli.exe来测试一下:redis-cli.exe -h 服务器IP –p 端口

  (8)既然每次都需要在命令窗口中开启Redis服务不爽,那我们就动手将其改为Windows服务,让它自动启动。通过在网上查找,我在CSDN找到了一个批处理文件和一个RedisService的可执行文件,并将这两个文件拷贝到指定的Redis目录(我的是D:/Redis):

  其中,install-service这个批处理文件的代码如下:

@echo off
set cur_path=%cd%
sc create redis-instance binpath= "\"%cur_path%\RedisService.exe\" %cur_path%\redis.conf" start= "auto" DisplayName= "Redis"

  意思是在我们的Windows中创建一个Redis的服务实例,指定要启动的程序路径与配置文件路径。这里需要注意的是:RedisService是另外的一个exe,不是我们刚刚下载下来就有的,这个RedisService.exe的下载地址为:http://pan.baidu.com/s/1sjrvmTf。启动模式设置为auto代表自动启动,显示的服务名称为Redis。这样,一个bat文件就做好了,点击运行之后,一个Redis的Windows服务也出现在了我们的Windows服务列表中,如下图所示:

四、小结

  随着新浪、赶集这些大型网站同一时刻的访问量逐步飙升,但它们却能做到让用户顺利的访问,运用的就是Redis的数据优化处理技术。因此,Redis经过了各大名企的考验,是一个高性能的NoSQL数据库,具有多种实用的用途。那么,我们.Net开发者是否可以借助Redis解决日常开发中遇到的高并发的问题呢,答案是肯定的。下一篇,我们会熟悉一下Redis为我们.Net开发者提供的API,以及Redis中提供的常见的数据类型(多种)等等。最后,衷心感谢本文的参考文献作者,特别是传智播客的王承伟老师,本文是基于其发布的公开课视频学习整理而成,感谢其提供了这么好的公开课教程!

参考文献

(1)传智播客Redis公开课,王承伟主讲,http://bbs.itcast.cn/thread-26525-1-1.html

(2)NoSQL百度百科,http://baike.baidu.com/view/2677528.htm

(3)孙立,《NoSQL开篇—为什么使用NoSQL》,http://www.infoq.com/cn/news/2011/01/nosql-why/

(4)Redis百度百科,http://baike.baidu.com/view/4595959.htm

(5)Ruthless,《Windows下安装Redis》,http://www.cnblogs.com/linjiqin/archive/2013/05/27/3101694.html

(6)张善友,《在Windows上以服务方式运行Redis》,http://www.cnblogs.com/shanyou/archive/2013/01/17/redis-on-windows.html

附件下载

(1)Redis-2.6服务包:http://pan.baidu.com/s/1dDEKojJ

(2)Redis注册Windows服务的批处理文件:http://pan.baidu.com/s/1jGJtNXs

(3)Redis注册Windows服务的启动程序(RedisServcie.exe):http://pan.baidu.com/s/1sjrvmTf

...
浏览 : 7857 阅读全文

开篇:经历了上一篇aspx与服务器控件探秘后,我们了解了aspx和服务器控件背后的故事。这篇我们开始走进WebForm状态保持的一大法宝—ViewState,对其刨根究底一下。然后,再对曾经很流行的ASP.Net AJAX方案中的利器—UpdatePanel这个神奇的区域一探究竟。

一、隐藏的状态—ViewState探秘

1.1 从Http的无状态说起

  Http是一个无状态协议,同一个会话的连续两个请求互相不了解,它们由最新实例化的环境进行解析,除了应用本身可能已经存储在全局对象中的所有信息外,该环境不保存与会话有关的任何信息。另外,因为,浏览器和服务器之间是通过Socket进行通信,Http请求通常请求完毕就会关闭Socket连接,因此Http协议不会保持连接。如果保持连接会降低客户端并发处理请求数,不保持连接又会降低处理速度(建立连接所需的时间会长一点);

PS:这里我们可以这样来理解:假如我们去一个大型商场购物购买某个产品,第一次去的时候是A销售员接待了我们,带领我们来到XX产品的柜台并为我们推荐了XX产品;等我们回去使用XX产品后,觉得XX产品真心不错。第二次我们又去,但是这次却找不到上次那个A销售员了,相反商场分配了另一个B销售员来接待我们,他不知道我们上次选择了XX产品,相反它却一个劲地向我们推荐YY产品并把我们带向YY产品的柜台;这个时候,我们一般会说:我擦,把上次那个妹子给我叫来!

  基于Http协议的无状态特性,我们在ASP.Net的开发中也会经常碰到这种情况:用户上一次提交的东西,下次再提交时服务器就不记得了。很多时候,我们感到很不解?后来,我们发现原来每一次的请求服务器都开启了不同的线程来处理,也就是说每次都会new一个XXX.aspx.cs中的类对象实例来进行处理(上一次new出来为我们处理的page对象也许早就被服务器销毁了)。比如,我们在xxx.aspx.cs代码中写入了一个int类型的number成员(初始为0),每次请求我们都想让这个number自增一下,然后重新返回给浏览器。但就是这么一个简单的梦想,我们却无法轻易的实现。

  那么,到底怎么来破呢?大神们已经为我们想好了策略,我们可以使用隐藏域字段、Cookie、Session等来保存状态。而伟大的Microsoft还在ASP.Net中帮我们封装了ViewState,以至于我们在WebForm中进行PostBack操作时,都感觉不到服务器是无状态的。

1.2 青春四处绽放—无处不在的ViewState

  (1)类似于Dictionary的一种数据结构

  如果你曾经使用过Dictionary或者Session的话,那么你应该了解这种Key/Value的数据结构。这里并没有什么高深的理论,ViewState通过String类型的数据作为索引。ViewState对应项中的值可以存储任何类型的值(参数是Object类型),实施上任何类型的值存储到ViewState中都会被装箱为Object类型。

  例如,这里我们可以改写上面那个按钮事件中的代码:

复制代码
 1 protected void btnGetNumber_Click(object sender, EventArgs e)
 2 {
 3     //number++;
 4     //this.lblNumber.Text = number.ToString();
 5 
 6     object age = this.ViewState["age"];
 7     if (age == null)
 8     {
 9         age = 1;
10     }
11     else
12     {
13         age = Convert.ToInt32(age) + 1;
14     }
15     this.ViewState["age"] = age;
16     this.lblNumber.Text = age.ToString();
17 }
复制代码

   这里,我们借助ViewState存储了age的状态值,第一次来我给你返回1,后面再来我就加1再返回给你。于是,在上一节我们所提到的那个问题(无法记住上次的number值,每次都返回1)就解决了。

PS:ViewState不能存储所有的数据类型,仅支持以下的这几种:
String、Integer、Boolean、Array、ArrayList、Hashtable以及一些自定义类型

  我们都知道,Dictionary和Session都是存储在服务器端的。那么,我们不禁要问,既然我们在服务器端给ViewState增加了一个Key/Value对,并返回给浏览器端,ViewState又是存储在什么位置的呢?

  (2)大隐隐于市的“页面级”隐藏字段

  跟Session和Dictionary的存储位置不同,ViewState的作用域是页面,也就是说ViewState是存储在浏览器的页面之中的(这里相比Session等,耗费的服务器资源较少,也算是ViewState的优点之一吧),当你关闭某个aspx文件后,那么属于这个aspx的ViewState也就不存在了。或许,这么说来,我们还不是很了解,现在我们来实地看看。

  ①首先,如果页面上有一个runat="server"的form,当用户请求这个页面时,服务器会自动添加一个_ViewState的隐藏域返回给浏览器。但是,我们发现这个ViewState的value看起来像一串乱码?这是为什么呢?这是因为服务器在向浏览器返回html之前,对ViewState中的内容进行了Base64的加密编码

  ②其次,当用户点击页面中的某个按钮提交表单时,浏览器会将这个_VIEWSTATE的隐藏域也一起提交到服务端;服务器端在解析请求时,会将浏览器提交过来的ViewState进行反序列化后填充到ViewState属性中(比如下图中,我们可以通过一个软件将_VIEWSTATE解码得到一个如下图所示的树形结构);再根据业务处理需要,从这个属性中根据索引找到具体的Value值并对其进行操作;操作完成后,再将ViewState进行Base64编码再次返回给浏览器端;

  ③因此,我们可以得出一个结论:VIEWSTATE适用于同一个页面在不关闭的情况下多次与服务器交互(PostBack)。这里我们也可以通过下图来温习一下ViewState的流程,ViewState存放着“事故现场”,下次可以方便地“还原现场”,将无状态的Http模拟成了有状态的,也让广大的初学者了解不到无状态的这个特性。

1.3 喜欢就会放肆—又爱又恨的ViewState!

  事实上,除了我们手动在服务器端向ViewState属性中添加的K/V对数据,我们在aspx.cs代码中为某些服务器控件设置的值(例如:为Repeater设置DataSource中存入的数据集、为Label所设置的Text内容等,但不包括:TextBox、CheckBox、CheckboxList、RadioButtonList)都存入了ViewState中。这样做的话,我们下次再向服务器提交请求时,现有表单中所有的服务器控件状态都会记录在ViewState中提交到服务器,在服务器端可以方便地对这些服务器控件进行有状态的操作并返回,这无疑是让我们欢喜的,因为方便了我们的开发过程,提高了我们的开发效率;

  但有人说:“喜欢就会放肆”,ViewState让人又爱又恨啊。例如,在我们使用Repeater的过程中,WebForm会自动将DataSource(数据源,你可以理解为一个集合)存储到ViewState中并返回给浏览器。可以参考下面的例子来实地理解一下:

  ①含有Repeater的aspx页面:

复制代码
 1     <form id="form1" runat="server">
 2         <div align="center">
 3             <table class="test">
 4                 <tr class="first">
 5                     <td>
 6                         ID
 7                     </td>
 8                     <td>
 9                         产品名称
10                     </td>
11                     <td>
12                         产品描述
13                     </td>
14                     <td>
15                         删除
16                     </td>
17                 </tr>
18                 <asp:Repeater ID="repeaterProducts" runat="server">
19                     <ItemTemplate>
20                         <tr>
21                             <td>
22                                 <%#Eval("Id") %>
23                             </td>
24                             <td>
25                                 <%#Eval("Name") %>
26                             </td>
27                             <td>
28                                 <%#Eval("Msg") %>
29                             </td>
30                             <td>
31                                 <a href='Product.ashx?Action=Delete&Id=<%#Eval("Id") %>'>删除</a>
32                             </td>
33                         </tr>
34                     </ItemTemplate>
35                 </asp:Repeater>
36             </table>
37         </div>
38     </form>
复制代码

  ②后台代码模拟从数据库中取得数据集合并绑定到Repeater中:

复制代码
 1 protected void Page_Load(object sender, EventArgs e)
 2 {
 3     if (!IsPostBack)
 4     {
 5         this.repeaterProducts.DataSource = this.GetProductList();
 6         this.repeaterProducts.DataBind();
 7     }
 8 }
 9 
10 private IList<Product> GetProductList()
11 {
12     IList<Product> productList = new List<Product>();
13     productList.Add(new Product() { Id = 1, Name = "康师傅方便面", Msg = "就是这个味儿!" });
14     productList.Add(new Product() { Id = 2, Name = "统一方便面", Msg = "还是那个味儿!" });
15     productList.Add(new Product() { Id = 3, Name = "白象方便面", Msg = "大骨浓汤啊!" });
16     productList.Add(new Product() { Id = 4, Name = "日本方便面", Msg = "不只是爱情动作片!" });
17     productList.Add(new Product() { Id = 5, Name = "台湾方便面", Msg = "马英九夸我好吃!" });
18 
19     return productList;
20 }    
复制代码

  编译生成后,通过查看此页面的html代码,可以明显看到一长串的_VIEWSTATE隐藏域。将此_VIEWSTATE复制到ViewStateDecoder中进行反编码,可以发现它确实存储了Repeater中的数据集合。这里我们不禁要问:展示数据既然已经渲染成了html,为何还要存储在ViewState隐藏域中?如果我们的数据集合是一百行、一千行数据的话,那ViewState隐藏域岂不很大(100k?200k?)?但不幸的是,这是ViewState的设计机制,要想依靠它来保持状态,它就会将服务器控件的状态包括数据集合都存储到其中,在浏览器和服务器之间来回传递保持状态。

  这里就涉及到网站的性能问题的探讨了:由于ViewState存储在页本身,因此如果存储较大的值,用户请求显示页面的速度会减慢(这对于互联网系统来说,就是一个噩梦。你会选择一个1秒内响应的网站浏览还是5秒内响应的网站?)。又因为ViewState会随同Form表单一同回传给服务器,如果ViewState很大的话,Http报文也会很大,网站流量消耗也会增大。

  那么,有没有一种方法可以让ViewState克制一下呢?别急,请看下面的介绍。

1.4 但爱就是克制—禁用还是不禁用ViewState?

  刚刚说到,因为ViewState会一定程度上影响性能,所以我们可以在不需要的时候禁用 ViewState。默认情况下 ViewState 将被启用,并且是由每个控件(而非页面开发人员)来决定存储在 ViewState 中的内容。有时,这一信息对应用程序并没有什么用处(例如上面提到的Repeater的数据集合,已经渲染生成了html显示,还存储了一份副本在ViewState里边)。尽管也没什么害处,但却会明显增加发送到浏览器的页面的大小。因此如果不需要用ViewState,最好还是将它关闭,特别是当 ViewState 很大的时候。当然,ViewState帮我们实现了某些服务器控件状态保持,因此在非必需的情况下,还是可以适度使用的,特别是在开发企业内部信息系统的场景。

  那么,怎样来禁用ViewState呢?禁用ViewState又有什么策略呢?下面我们一一来探讨。

  ①页面级禁用ViewState:在aspx的首部的Page指令集中添加EnableViewState="false",该页面中所有控件的状态都不会存入ViewState的,页面一下就会清爽许多;

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="RepeaterViewState.aspx.cs"
    Inherits="WebFormDemo.RepeaterViewState" EnableViewState="false" %>

  禁用后,再次查看生成的html代码,我们会发现:咦,_VIEWSTATE还在那儿,但是明显比先前的体积小了不少

  再将这个瘦身后的_VIEWSTATE复制到ViewStateDecoder中进行反编码查看,我们会发现,只保存了一个最基本的信息,Repeater的那些数据集合没有存入进去了。

PS:为什么禁用ViewState之后,页面源代码中仍然有_VIEWSTATE的隐藏域

这是因为就算禁用了viewstate,aspx页面中还是会有一个服务器控件在那里使用,这就是<form runat="server">。这时,如果你将form去掉runat="server",将其变为普通html标签,那么页面就干净了,从此_VIEWSTATE这个隐藏域彻底消失在你的页面中。  

  ②控件级禁用ViewState:在某些场景中,我们只希望禁用某个控件(例如Repater)的ViewState,其他控件仍然通过ViewState保持状态。这时,我们可以给指定的控件设置一个属性EnableViewState="false"即可;

<asp:Repeater ID="repeaterProducts" runat="server" EnableViewState="false">
</asp:Repeater>

  ③全局级禁用ViewState:园子里的大神老赵(Jeffrey Zhao)曾经说过,“我如果新建一个WebForm项目,做的第一件事情就是去Web.config中将enableViewState设置为false从而将ViewState全局关闭”。那么,我们如果希望将网站中所有页面的ViewState都禁用,总不可能去一个一个页面得修改Page指令吧?ASP.Net为我们提供了一个配置,我们只需要在Web.config的system.web中增加一句配置即可:

<pages enableViewState="false" />

PS:开发中也可以采用大神老赵的做法,先禁用,再选择性启用,毕竟没有非要ViewState才能干成的事儿!

  ④真正的禁用ViewState:刚刚我们的三种方法实践后,在页面还是出现_VIEWSTATE的隐藏域,尽管它保留了最基本的信息。那么,我们可能会问?怎样才能彻底地真正地禁用ViewState,根本就别给我生成_VIEWSTATE的隐藏域。答案是有的,将<form runat="server"/>的runat="server"去掉,就不会出现了,但那样又会偏离WebForm的开发模式,大部分的服务器控件都无法正常使用,开发效率又会有所损失。

  综上所述,在实际开发中应该权衡利弊,特殊情况特殊分析(到底这个场景该不该禁用ViewState),选择是否禁用ViewState,采用何种方式禁用ViewState。对于ViewState的探秘本篇就到此为止,由于我本人理解的也不是很深刻,所以希望各位园友如果有理解,可以回复出来大家探讨共同进步。

二、飞来的利器—UpdatePanel探秘

2.1 从一个简单四则运算计算器说起

  假如有以下一个场景,我们要做一个简单的四则计算器。aspx页面代码和后端逻辑代码如下:

  (1)aspx页面代码

复制代码
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>AJAX计算器</title>
</head>
<body>
    <form id="form1" runat="server">
    <div align="center">
        <asp:TextBox ID="txtNumber1" runat="server"></asp:TextBox>
        <asp:DropDownList ID="ddlFunc" runat="server">
            <asp:ListItem Value="0">+</asp:ListItem>
            <asp:ListItem Value="1">-</asp:ListItem>
            <asp:ListItem Value="2">*</asp:ListItem>
            <asp:ListItem Value="3">/</asp:ListItem>
        </asp:DropDownList>
        <asp:TextBox ID="txtNumber2" runat="server"></asp:TextBox>
        <asp:Button ID="btnGetResult" runat="server" Text="=" Width="50" 
            onclick="btnGetResult_Click" />
        <asp:Label ID="lblResult" runat="server" Text="" Font-Bold="true"></asp:Label>
    </div>
    </form>
</body>
</html>
复制代码

  (2)后置逻辑代码

复制代码
public partial class AjaxCalculator : System.Web.UI.Page
{
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        protected void btnGetResult_Click(object sender, EventArgs e)
        {
            int number1 = Convert.ToInt32(this.txtNumber1.Text);
            int number2 = Convert.ToInt32(this.txtNumber2.Text);
            int result = 0;
            switch(this.ddlFunc.SelectedValue)
            {
                case "0":
                    result = number1 + number2;
                    break;
                case "1":
                    result = number1 - number2;
                    break;
                case "2":
                    result = number1 * number2;
                    break;
                case "3":
                    if(number2 == 0)
                    {
                        throw new DivideByZeroException("除数不能为0!");
                    }
                    result = number1 / number2;
                    break;
            }
            this.lblResult.Text = result.ToString();
        }
}
复制代码

  生成后运行该页面,可以达到以下的效果。我们输入两个数字后,选择是加法、减法、还是乘除法后,点击=按钮,即可刷新页面显示运算结果。

  在WebForm中,每一次点击runat="server"的按钮都会将调用form.submit将请求提交到服务器,服务器会返回新的页面html进行页面重绘。这是一个整页的刷新操作,不符合AJAX的风格需求。因此,我们想要将其改为AJAX版本的,除了使用基本的XMLHttpRequest外,我们还可以使用基于JQuery的AJAX方案,这些都是轻量级的原生态的AJAX技术方案。但我们伟大的微软(我哭啊,真是为我们考虑啊,连AJAX方案都为我们解决了,而且还提供了AJAX控件供我们使用,我们拖控件的习惯可以用到AJAX方案上了!!!)还为我们提供了一套叫做ASP.Net AJAX的技术方案,通过这套方案,我们可以在ASP.Net很容易地实现AJAX效果,甚至都不需要我们懂JavaScript。因此,也就出现了前些年,很多WebForm开发者陆续使用ASP.Net AJAX Extension进行AJAX开发,纷纷表示:AJAX如此简单,我等岂能不会?但是,虽然它简单易行,由于其性能问题一直被人诟病,而我们这些菜鸟也未能了解其性能问题的原因,本着知其然也知其所以然的目标,现在我们来使用它并剖析它一下。

2.2 天上掉下个林妹妹—使用UpdatePanel控件

  不得不说,UpdatePanel真的是天上掉下的林妹妹,一个神奇的控件!有了它,我们可以将页面中需要进行局部刷新的内容放到其ContentTemplate中,一个需要整页刷新的操作便可以成为局部刷新。现在,我们首先来使用其改造刚刚的简单四则计算器页面。

  (1)加入UpdatePanel,并将计算器html内容拖入ContentTemplate中

复制代码
 <form id="form1" runat="server">
    <div align="center">
        <asp:ScriptManager ID="scriptManager" runat="server">
        </asp:ScriptManager>
        <asp:UpdatePanel ID="updatePanel" runat="server">
            <ContentTemplate>
                <asp:TextBox ID="txtNumber1" runat="server"></asp:TextBox>
                <asp:DropDownList ID="ddlFunc" runat="server">
                    <asp:ListItem Value="0">+</asp:ListItem>
                    <asp:ListItem Value="1">-</asp:ListItem>
                    <asp:ListItem Value="2">*</asp:ListItem>
                    <asp:ListItem Value="3">/</asp:ListItem>
                </asp:DropDownList>
                <asp:TextBox ID="txtNumber2" runat="server"></asp:TextBox>
                <asp:Button ID="btnGetResult" runat="server" Text="=" Width="50" OnClick="btnGetResult_Click" />
                <asp:Label ID="lblResult" runat="server" Text="" Font-Bold="true"></asp:Label>
            </ContentTemplate>
        </asp:UpdatePanel>
    </div>
</form>
复制代码

  (2)运行该页面,通过开发人员工具查看Http请求

  通过查看请求报文,我们了解到此次的请求响应不再是返回整页的html内容,而只是我们放在了UpdatePanel里面的html内容,页面也没有再刷新,于是不禁感叹一句:AJAX,So easy!妈妈再也不用担心我的页面了!

2.3 直到看见XmlHttpRequest才是唯一的答案—UpdatePanel原来如此

  正当我们沉浸在UpdatePanel为我们提供的神奇的AJAX世界里时,我们不禁对UpdatePanel为我们做了哪些工作产生了兴趣。

  (1)首先,我们知道AJAX的核心对象是XmlHttpRequest,那么原生态的AJAX请求的JS方法是如何写的呢?

复制代码
function ajax(url, onsuccess) {
    var xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); //创建XMLHTTP对象,考虑兼容性。XHR
    xmlhttp.open("POST", url, true); //“准备”向服务器的xx.ashx发出Post请求(GET可能会有缓存问题)。这里还没有发出请求

    //AJAX是异步的,并不是等到服务器端返回才继续执行
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState == 4) //readyState == 4 表示服务器返回完成数据了。之前可能会经历2(请求已发送,正在处理中)、3(响应中已有部分数据可用了,但是服务器还没有完成响应的生成)
        {
            if (xmlhttp.status == 200) //如果Http状态码为200则是成功
            {
                onsuccess(xmlhttp.responseText);
            }
            else {
                alert("AJAX服务器返回错误!");
            }
        }
    }
    //不要以为if (xmlhttp.readyState == 4) {在send之前执行!!!!
    xmlhttp.send(); //这时才开始发送请求。并不等于服务器端返回。请求发出去了,我不等!去监听onreadystatechange吧!
}
复制代码

  (2)其次,通过查看运行页面的html,我们可以发现加入UpdatePanel后,我们的html中多了这么几个js引用。

  (3)既然我们知道要发AJAX请求,必然会涉及到XmlHttpRequest。那么,我们就在这几个js中取看看是否有涉及到XmlHttpRequest。通过查看,我们找到了这样一个似曾相识的js方法:

复制代码
function Sys$Net$XMLHttpExecutor$executeRequest() {
        /// <summary locid="M:J#Sys.Net.XMLHttpExecutor.executeRequest" />
        if (arguments.length !== 0) throw Error.parameterCount();
        this._webRequest = this.get_webRequest();
        if (this._started) {
            throw Error.invalidOperation(String.format(Sys.Res.cannotCallOnceStarted, 'executeRequest'));
        }
        if (this._webRequest === null) {
            throw Error.invalidOperation(Sys.Res.nullWebRequest);
        }
        var body = this._webRequest.get_body();
        var headers = this._webRequest.get_headers();
        this._xmlHttpRequest = new XMLHttpRequest();
        this._xmlHttpRequest.onreadystatechange = this._onReadyStateChange;
        var verb = this._webRequest.get_httpVerb();
        this._xmlHttpRequest.open(verb, this._webRequest.getResolvedUrl(), true );
        this._xmlHttpRequest.setRequestHeader("X-Requested-With", "XMLHttpRequest");
        if (headers) {
            for (var header in headers) {
                var val = headers[header];
                if (typeof(val) !== "function")
                    this._xmlHttpRequest.setRequestHeader(header, val);
            }
        }
        if (verb.toLowerCase() === "post") {
            if ((headers === null) || !headers['Content-Type']) {
                this._xmlHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
            }
            if (!body) {
                body = "";
            }
        }
        var timeout = this._webRequest.get_timeout();
        if (timeout > 0) {
            this._timer = window.setTimeout(Function.createDelegate(this, this._onTimeout), timeout);
        }
        this._xmlHttpRequest.send(body);
        this._started = true;
}
复制代码

  由以上的方法名我们可以猜到,此方法是一个执行AJAX请求的方法。在此方法中,创建了XmlHttpRequest对象,也使用了open方法指明以GET还是POST方法向服务器哪个处理程序发送请求,并且也为该请求指定了请求成功后需要执行的回调函数方法(onreadystatechange),最后调用send方法正式发送请求

  由此,我们可以初步分析出一个结论:UpdatePanel本质还是帮我们封装了以XmlHttpRequest为核心的一系列方法帮我们将CodeBehind中的同步事件变为了异步操作,并通过DOM更新指定的HTML内容,使得我们可以方便地实现AJAX效果

  但是,我们也不由发出感叹:本来可以很简单地使用XmlHttpRequest来实现的东西,为什么使用UpdatePanel会引入这么多js,并且为我们返回的东西还是那么多(比如上面的例子,我只需要的数据是一个结果,却给我返回一部分无用的html,还有一系列的hiddenId之类的数据)。在对性能要求较高的应用场合,如果使用UpdatePanel来实现AJAX会增加服务器的负载,并且会消耗掉不必要的网络流量(比如每次请求都会来回都会发送ViewState里的数据,在性能和数据量上都会造成损失)。园子里的浪子曾经在他的博文《远离UpdatePanel带给我的噩梦》里边写到:“UpdatePanel在页面小的时候还是很好用的,而当页面控件数不断上升的时候,UpdatePanel就开始直线下降,我们现在页面有4,5百个控件,每做一次PostBack需要长达15秒钟之长,实在让人无法忍受。”

  那么,有木有方式可以替换UpdatePanel呢?其实答案很简单,那就是使用基于XmlHttpRequest的js方法,再加上一定的js回调函数即可。这就要求我们掌握javascript,不能只做拖UpdatePanel控件的程序员。现在基于js的JQuery库也早已为我们封装了XmlHttpRequest,提供了ajax开发的一系列方法供我们调用,相当于UpdatePanel的“重量级”来说,可谓是轻了不少,是一个“轻量级”的AJAX开发方式。通过借助jQuery Ajax+ashx可以方便地在.Net中进行Ajax开发,并且具有不错的性能,这也是我实习所在的企业中经常用到的方式。

三、学习总结

  本篇主要学习了WebForm中的状态保持法宝—ViewState,以及曾经的ASP.Net AJAX方案的利器—UpdatePanel,虽然一直在说这个不好,那个别用。但是,微软之所以为我们提供了这些东西,肯定有它存在的理由,并不一定都是不好的东西。所谓利器在手,没有一点内功心法的人还是使用不好它,无法发挥出其100%的优势。因此,身为.Net学习者的我们,不能满足于微软为我们所提供的便利,要知其然也知其所以然,做一个上进的程序员,加油吧!

  校园招聘的大潮就快来临,希望园子里跟我一样即将毕业的菜鸟们能够好好复习基础,在招聘中赢得一份好offer,实现自己的价值!

参考资料

  (1)杨中科,《特供2014版ASP.Net教程》,http://net.itcast.cn/subject/tegongnet/index.html

  (2)匿名未知,《状态保存机制之ViewState概述及应用》,http://www.jb51.net/article/33686.htm

  (3)popping_dancer,《ASP.Net原理篇之ViewState》,http://www.2cto.com/kf/201210/160413.html

  (4)玉开,《ASP.Net 4.0新特性-输出更纯净的Html代码》,http://www.cnblogs.com/yukaizhao/archive/2010/05/22/asp-net-new-feature-pure-html.html

  (5)Infinities Loop,《ASP.Net ViewState详解》,http://www.cnblogs.com/wwan/archive/2010/11/18/1880357.html

  (6)LifelongLearning,《ASP.Net 4.0中ViewState使用简介》,http://www.csharpwin.com/dotnetspace/13220r6527.shtml

  (7)自由飞,《禁用VIEWSTATE之后(一)》,http://www.cnblogs.com/freeflying/archive/2009/12/28/1634229.html

  (8)MSDN,《了解使用ASP.NET AJAX进行局部页面刷新》,http://msdn.microsoft.com/zh-cn/dd361854.aspx

  (9)xiaomin,《UpdatePanel工作原理》,http://www.cnblogs.com/xiaomin/archive/2011/11/01/2232215.html

  (10)浪子,《远离UpdatePanel给我的噩梦》,http://www.cnblogs.com/walkingboy/archive/2007/01/09/615691.html

 

...
浏览 : 8223 阅读全文

开篇:毫无疑问,ASP.Net WebForm是微软推出的一个跨时代的Web开发模式,它将WinForm开发模式的快捷便利的优点移植到了Web开发上,我们只要学会三步:拖控件→设属性→绑事件,便可以行走于天下。但这样真的就可以走一辈子吗?实际上,ASP.Net经常被喷的诟病就在于WebForm以及只会拖控件的ASP.Net程序员,往往大型互联网系统也没有采用WebForm的模式进行开发。但是,WebForm并不是一无是处,而是我们没有用好,还有很多东西我们知其然不知其所以然,现在我们就来对这些平时所不注意但又十分关键的东西一探究竟。

一、神秘不神秘—aspx探秘

1.1 WebForm时代的请求对象

aspx

  在WebForm中,所有的页面请求都是以aspx文件作为请求对象(静态化和伪静态的除外)。例如上图中,访问者在浏览器端通过输入URL:blog/index.aspx向服务器端发送请求,服务器端首先找到这个index.aspx,然后创建页面对象(index.aspx.cs文件中的类对象),调用这个页面对象中的ProcessRequest方法和Page_Load方法(在此过程中,有可能需要访问数据库)来生成aspx页面的所有html内容,最后将生成好的html返回给浏览器端。

  因此,我们可以知道,服务器端对aspx的处理过程其实就是一个渲染生成html的过程

1.2 神奇的<%%>

  通过实践可知,在aspx中除了<%%>的内容和runat="server"的内容,其他都是原样输出。这是因为我们在aspx中可以借助<%%>写入C#代码,就跟ASP、PHP一样的风格。但是,在实际开发中并不建议这么来做,因为它违反了CodeBehind的原则,不利于职责的分离。

  ①直接写入C#业务逻辑代码

1         <%
2             for (int i = 0; i < 5; i++)
3             {
4                 Response.Write("I am a webform page.<br/>");
5             }
6         %>

  ②获取C#方法的返回值

  假设页面后端代码中有一个GetServerTime的方法,它只有一句代码:return DateTime.Now.ToString();。页面中只需要通过<%= 方法名() %>即可获取该方法的返回值。

<%= GetServerTime() %>

  ③aspx中能够访问的方法的访问修饰符只能为public和protected:这是因为aspx和aspx.cs之间的关系是编译生成后aspx和aspx.cs会创建两个类,并且aspx继承自aspx.cs中的类,在面向对象中子类要访问父类的方法,那么方法的访问修饰符必须为public或protected。(后面会讲到aspx和aspx.cs的关系,不要急)

1.3 aspx与ashx的关系

  ashx是一般处理程序,它是一个实现了IHttpHandler的轻量级处理程序,处理操作都在ProcessRequest方法中完成。

  而aspx则相当于一个特殊的、高级的ashx,aspx所对应的父类是System.Web.UI.Page这个类,通过查看Page类的定义,我们可以看到Page也实现了IHttpHandler的这个接口。另外之所以说它是高级的ashx,是因为aspx帮我们封装了许多底层的操作,使得我们可以进行傻瓜式的开发操作。

  看到这里,我们不禁要问:既然有了ashx为何还要aspx?大家都知道ashx中的ProcessRequest方法需要向请求响应报文中输出html,而每个html页内容有很多,如果每次响应都往里边输出html开发起来会很痛苦(这里主要是指在如果不借助模板引擎的情况下),而aspx则起到了类似于于一个模板引擎的作用,帮我们把html的大体框架定义好了,我们在开发中就只需要操作每次响应需要更改的内容即可。

1.4 aspx与aspx.cs的关系

  (0)假如我们有以下的名为FirstPage的一个aspx页面:

复制代码
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>第一个WebForm页</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        哈哈,我是ASP.Net WebForm,下面看我的表演。
        <br />
        <%
            for (int i = 0; i < 5; i++)
            {
                Response.Write("I am a webform page.<br/>");
            }
        %>
        <br />
        <%= GetServerTime() %>
        <br />
        <asp:TextBox ID="txtDateTime" runat="server"></asp:TextBox>
        <asp:Button ID="btnGetTime"
            runat="server" Text="获取时间" onclick="btnGetTime_Click" />
        <br />
        <% GetDllInfo(); %>
    </div>
    </form>
</body>
</html>
复制代码

  其后台代码.cs文件代码如下:

复制代码
namespace WebFormDemo
{
    public partial class FirstPage : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        protected string GetServerTime()
        {
            string result = "服务器时间:" + DateTime.Now.ToString();
            return result;
        }

        protected void GetDllInfo()
        {
            Response.Write("页面类名称:"+this.GetType() + "<br/>");
            Response.Write("程序集地址:"+this.GetType().Assembly.Location + "<br/>");
            Response.Write("父类的名称:"+this.GetType().BaseType + "<br/>");
            Response.Write("程序集地址:"+this.GetType().BaseType.Assembly.Location + "<br/>");
        }

        protected void btnGetTime_Click(object sender, EventArgs e)
        {
            txtDateTime.Text = DateTime.Now.ToString();
        }
    }
}
复制代码

  (1)CodeBehind:在每个aspx文件中的头部,我们都会看到以下的一句代码

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="FirstPage.aspx.cs" Inherits="WebFormDemo.FirstPage" %>

  其中CodeBehind这个属性定义了此aspx页面的专属后台代码文件的名称,而Inherits这个属性则定义了此aspx页面所要继承的父类的名称(这也可以简单地说明,aspx页面会单独生成一个类,与后台代码类不重合在一起)。因此,aspx.cs就是aspx的后置处理代码,负责处理aspx中<%%>和runat="server"的内容。

  (2)子类与父类:我们使用ASP.NET写的网站在运行时候都会被编译生成为一个一个的程序集(.dll),而我们的aspx页面也会被生成为一个一个的类。那么,我们如何来证明aspx会生成一个类,而且还是aspx.cs中的类的子类呢?那么,我们需要反编译系统所生成的程序集(.dll)文件。

  第一步:找到网站所生成的程序集

  我们可以通过写入以下代码,然后在aspx中<% GetDllInfo(); %>调用;

复制代码
protected void GetDllInfo()
{
    Response.Write("页面类名称:"+this.GetType() + "<br/>");
    Response.Write("程序集地址:"+this.GetType().Assembly.Location + "<br/>");
    Response.Write("父类的名称:"+this.GetType().BaseType + "<br/>");
    Response.Write("程序集地址:"+this.GetType().BaseType.Assembly.Location + "<br/>");
}
复制代码

  浏览页面,会显示以下结果:通过下图可以看到,我们的FirstPage这个页面会生成一个ASP.firstpage_asx的类,其父类是FirstPage。

PS:当某个页面第一次被访问的时候,CLR就会使用一个代码生成器去解析aspx文件并生成源代码并编译,然后以后的访问就直接调用编译后的dll,这也是为什么aspx第一次访问的时候非常慢的原因。  

  第二步:反编译临时程序集文件

  ①通过上面显示的路径找到dll,并拖到反编译工具(ILSpy或者Reflector,前者开源免费,后者已经收费,但天朝,你懂的。)进行查看。通过下图可以看出,页面类aspx是后台代码类所绑定的子类,它的名称是aspx文件名加上“_aspx”后缀。因此,这里也就解释了为什么在aspx中要访问的方法必须是public和protected的访问修饰符才可以。

  ②下图则展示了对页面后置代码类所在的程序集进行反编译的情况:

  第三步:我们在刚刚时就说了,服务器端对aspx处理的过程是一个渲染生成html的过程,如何来深入理解这句话,我们可以在此借助反编译工具来一探究竟。通过对aspx类的反编译,我们可以看到在它的方法列表中有如下几个命名格式一样的方法:

  ①_BuildControl_controlX(); X代表数字

  通过对这几个方法的源码分析,我们可以知道,这些方法都在做一件事件:拼接生成aspx页面的html内容。每个方法都会返回一个控件类型的对象,有LiteralControl类型,也有HtmlHead类型(在aspx中只要给head加了runat="server"就会有此类型的生成方法)等等,那么这些数字又代表了什么?难道是生成html的执行顺序么?这些生成方法又是在哪个方法中被一一调用的呢?别急,下面我们就来看看。

  ②通过后面几个方法源码的查看,我们发现原来上面的几个生成控件的方法都在一个叫做BuildControlTree的方法(生成控件树)中被依次调用。

  这里几乎是按照数字序号的顺序来依次调用具体的BuildControl_controlX()方法,并将每次返回的控件添加到页面中去。这里可以看到,BuildControlTree方法的参数是其本身,它实现了IParserAccessor的接口。这里暂且将这个接口其理解为一个大的控件容器,可以往这个容器里边添加子控件(这里看到不同类型的控件都可以往里边加,那么肯定初步断定方法参数应该是object类型),这里将每次调用BuildControl_controlX()方法所返回的控件类型添加到了这个容器中。

  ③刚刚分析了BuildControlTree方法,知道了控件的生成过程。但是,页面主体内容又在哪里呢?服务器端要返回的内容可不止是那些控件的HTML代码啊。别急,通过查看反编译的方法,我们看到原来Renderform1这个方法里边。PS:这里方法名为什么是form1呢?那是因为我们在aspx中给form表单设置的ID就为form1。

  ④这里我们就分析到这儿,而WebForm具体的页面生命周期留到后面的ASP.Net页面生命周期探索的文章中详细介绍。这里我们只需要知道,aspx这个类是其后置代码类的子类,它要做的工作就是帮我们生成要返回浏览器端的html内容即可。其中,RenderForm将渲染生成整个form表单,而BuildControlTree则会生成服务器控件树,以便在后面的方法中方便地调用每个控件的RenderControl方法生成html字符串。

二、好用不好用—服务器控件探秘

2.1 企业中到底在用哪些控件?

  企业项目中经常使用到的最多还是一些“轻量级”的控件,例如:Button、TextBox、CheckBox、RadioButton、DropDownList、Repeater、ListView等;就我所实习的单位来说,这一年做WebForm的项目以来,用的最多也就是这些控件,数据控件除了Repeater就没用过其他的。我觉得数据控件的话,好好学习下Repeater就够了,因为Repeater已经足够强大了。至于什么***DataSource、Validator、Wizard、Login还有什么ASP.Net AJAX ToolKit就根本没杂用,这些控件既复杂又不实用,而且还比较重量级。

PS:有关Repeater控件的详细学习,可以参考w3school的教程:http://www.w3school.com.cn/aspnet/aspnet_repeater.asp

2.2 需要注意的基本控件用法

  (1)Button控件中的OnClientClick属性

  ①在WebForm中,Button控件有两个Click事件:一个是OnClick的服务端事件,另一个是OnClientClick的客户端事件;OnClick事件写在后置代码类中,每次点击Button首先会触发OnClientClick事件(OnClientClick会返回一个bool值,为true则继续执行OnClick,为false则不继续)。

  ②通过分析这个属性,可以知道OnClientClick是一个字符串属性,写的代码是JavaScript代码,在上面所说的BuildControl方法中会渲染成input的onclick方法,它会运行在浏览器端。

复制代码
 1 <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ClientClickPage.aspx.cs"
 2     Inherits="WebFormDemo.ClientClickPage" %>
 3 
 4 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 5 <html xmlns="http://www.w3.org/1999/xhtml">
 6 <head runat="server">
 7     <title>OnClientClick</title>
 8     <script type="text/javascript">
 9         function checkConfirm() {
10             var flag = confirm("您确定要删除此内容?");
11             return flag;
12         }
13     </script>
14 </head>
15 <body>
16     <form id="form1" runat="server">
17     <div>
18         <asp:Label ID="lblFlag" runat="server" Text="This is a label control text."></asp:Label>
19         <asp:Button ID="btnDelete" runat="server" Text="Delete" 
20             OnClientClick="return checkConfirm()" onclick="btnDelete_Click" />
21     </div>
22     </form>
23 </body>
24 </html>
复制代码

  在上面的Button控件中,既设置了OnClientClick也设置了OnClick服务端事件,浏览生成的页面源代码,可以看到在生成的html中,OnClientClick确实是渲染成了input的onclick这个浏览器端的事件:在Button每次以POST方式向服务器提交请求之前,都会先进行checkConfrim这个方法的判断,如果返回值为true才会将请求提交到服务器端;

  (2)被某些人滥用的LinkButton

  ①LinkButton用法跟Button差不多,区别就只在于LinkButton渲染成超链接(<a></a>),而Button渲染生成input标签(<input type="button"></input>)。

  ②不要用LinkButton来实现普通的超链接,在实际开发中,我还真见过有些人用LinkButton来实现超链接的:他们在LinkButton的OnClick事件中写Response.Redirect("xxx.aspx");这种页面跳转代码,完全是作死的节奏啊。能在浏览器端进行的事儿为啥要弄到服务器端来进行呢?而且就只是一个页面跳转的小事。

2.3 AutoPostBack的那点事

  (1)什么是PostBack

  比如现在正在访问a.aspx这个页面上,点击页面上的某个submit按钮把数据提交到a.asx.cs进行处理,这个过程则可以看作是:“从客户端浏览器把之前的状态数据提交回来(PostBack)”。

PS:设置了runat="server"的Button或者input控件都会渲染生成type="submit"的按钮

  (2)刚刚提到只有点击submit类型的按钮才会提交请求到服务器,那么在以下这种场景如何破呢?

复制代码
 1 <form id="form1" runat="server">
 2     <div>
 3         <asp:DropDownList ID="ddlProvince" runat="server" 
 4         onselectedindexchanged="ddlProvince_SelectedIndexChanged">
 5             <asp:ListItem Value="BJ">北京市</asp:ListItem>
 6             <asp:ListItem Value="CQ">重庆市</asp:ListItem>
 7             <asp:ListItem Value="SC">四川省</asp:ListItem>
 8         </asp:DropDownList>
 9         <asp:DropDownList ID="ddlCity" runat="server">
10             <asp:ListItem Value="-1">请先选择省份</asp:ListItem>
11         </asp:DropDownList>
12     </div>
13 </form>
复制代码

  有一个省市两级联动的下拉列表场景,在用户选择一个省份后,自动从服务器获取属于该省份的市名下拉列表。这里使用了DropDownList控件,该控件提供了一个叫做SelectIndexChanged的事件,它会帮我们渲染生成select的onchange的浏览器事件。但是在页面的浏览过程中,我们怎么选择不同的省份,市名称的下拉列表就是不动,因为没有向服务器提交数据请求。

  ①这时候,一位名叫MSDN的大神会告诉你,需要给这个DropDownList控件设置一个AutoPostBack="true"的属性,经调试后果然可行了。但是,作为一个学习者,我们会想知道到底这个AutoPostBack帮我们做了什么事儿?这时,我们可以通过查看浏览器的源代码一探究竟。

  ②通过浏览器提供的开发人员工具查看数据请求报文,可以看到除了提交form中的input外,还提交了ASP.Net WebForm预置的一些隐藏字段,而这些隐藏字段则是WebForm为我们提供便利的基础。比如EventTarget则记录刚刚提交给服务器的是哪个服务器控件。

2.4 为什么需要IsPostBack

  注:WebForm页面中如果有一个runat="server"的form,那么必定会涉及到IsPostBack。

  (1)Http的无状态:因为Http是无状态的,所以这次会话结束下次再给我提交请求我也不记得你是谁,即使你是李刚的儿子,老子也不认识。那么,为了解决这种问题,我们可以使用一些方法来解决,例如设置一个隐藏字段来判断,如果是PostBack那么肯定请求报文中会带上这个字段,如果不是那么请求报文中肯定没有这个字段。比如,下面我们使用隐藏字段来作为判断PostBack的标志。

 <input name="IsPostBack" type="hidden" value="true" />

  (2)ASP.Net WebForm中内置了一个IsPostBack属性(bool类型),我们可以在Page_Load事件中判断IsPostBack是否为true,如果不为true则可以知道是第一次访问或者是请求页面的操作,而如果为true则代表是PostBack操作,我们可以分别进行不同的业务逻辑处理。例如:有的代码只会在页面第一次加载时才执行(比如从数据库中读取数据并显示),这时就应该使用IsPostBack进行判断。

复制代码
if (!IsPostBack)
{
    this.lblInfo.Text = "第一次来,不是PostBack";
}
else
{
    this.lblInfo.Text = "非第一次来,是PostBack";
}
复制代码

  (3)通过查看生成的页面html代码,我们没有发现页面中有IsPostBack的这个隐藏字段。那么,它是存储在哪个位置又是根据什么来判断的呢?实际上,IsPostBack属性是根据ViewState中的一些特殊的键值对来判断赋值的(因为:每次提交请求后,服务器端都会返回不同的ViewState隐藏域给浏览器端;同样,浏览器每次也会将ViewState提交给服务器端,服务器端会解析ViewState还原上次状态)。对于ViewState,可以通过一些软件例如ViewStateDecoder进行解析查看,例如下图:

  如果我们禁用了ViewState,那么也就无法正常使用IsPostBack属性了,也无法正常使用PostBack了。那么对于ViewState,我会在下一篇进行简单探秘,本篇就到此为止。

  如果你觉得本文对你有用,那就麻烦点个“推荐”吧,也能让我更有动力写下去,谢谢!

附件下载

  (1)反编译利器ILSpy:https://github.com/icsharpcode/ILSpy/releases/download/2.2/ILSpy_2.2.0.1706_Binaries.zip

  (2)aspx揭秘的WebFormDemo:http://pan.baidu.com/s/1ntqOl4H

  (3)服务器控件揭秘WebFormDemo:http://pan.baidu.com/s/1qWFK8cW

...
浏览 : 9893 阅读全文

ASP.Net开发基础温故知新学习笔记

Mon, 01 Dec 2014 19:19:10 GMT

申明:本文是学习2014版ASP.Net视频教程的学习笔记,仅供本人复习之用,也没有发布到博客园首页。

一、一般处理程序基础

  (1)表单提交注意点:

    ①GET通过URL,POST通过报文体;

    ②需在HTML中为表单元素设置name;

    ③元素id是给Dom用的,name才是提交给服务器用的;

  (2)请求处理响应模型:

    ①浏览器发出访问请求→②服务器处理访问请求并返回HTML→③浏览器解析HTML并显示页面

  (3)GET与POST的区别(★★★→重点)

    ①GET通过URL传值,而POST通过HTTP报文;

    ②GET传递的数据量有限,POST则没有限制;

    ③POST方式无法通过URL在其他用户中还原;

    ④GET方式URL传特殊字符需要事先进行编码;

  (4)HTTP协议基本理解:

    ①连接(Connection):HTTP不保持连接(请求完成就关闭),如果保持连接会降低客户端并发处理请求数,不保持连接会降低处理速度(建立连接速度很慢);

    ②请求(Request):包含请求类型、请求的数据以及客户端信息等;

    ③响应(Response):包含具体HTML、响应是否成功以及错误码等;

二、模板引擎开发基础

  (1)传统模式的缺点:

    ①没有实现界面和逻辑的分离,美工无法介入;

    ②占位符替换不够灵活,无法进行复杂的替换;

  (2)NVelocity模板引擎:

    ①基本用法:编写模板→提供数据→渲染生成HTML

    ②扩展用法:include与parse的区别?

      →#include("head.htm")代表模板嵌套子模板;#parse("foot.htm")代表模板嵌套子模板,子模板可继承父模板中的参数;

    ③注意之处:

      NVelocity解析JQuery代码$.ajax中的$时把$当做NVelocity中的特殊符号,应对方法是使用jQuery.ajax代替$.ajax;

      如果要将DataTable传递给NVelocity时仅传递DataTable.Rows即可,因为Rows才是一个Collection(集合),可以使用foreach遍历;

      为了减少每次NVelocity解析模板的时间建议启用NVelocity缓存;

三、状态的传递与保持

  (1)经典的URL传递:

    ①优点:简单直接,明确发给谁,数据不会乱;

     ②缺点:无法保密,安全性不高

  (2)隐藏字段传递:

    ①会加大网站流量;

     ②会降低访问速度,想想ViewState

     ③机密数据无法保证安全性;

  (3)Cookie:(★★★→重点)

    ①基本概念:保存在浏览器端,每次向服务器提交请求时都会带上Cookie;服务器返回报文除了Html外还有更新后的Cookie;

     ②生命周期:如果没有设定Expires过期时间,那么关闭浏览器则终止Cookie;如果设定了Expires过期时间,则以过期时间为准作为失效时间;

     ③缺点限制:存储数据量有限,机密信息不能存在Cookie中;无法跨越不同的浏览器,例如:IE、Chorme、Firefox等;可以被清除,不要将不能丢失的数据存到Cookie;

  (4)Session:(★★★→重点)

     ①基本概念:服务器端的“Cookie”,类似于病历本;

     ②生命周期:Session具有自动销毁机制;

     ③使用注意:HttpHandler要使用Session需实现IRequiresSessionState接口;存放在服务器内存中,不要存放大数据;

    ④与Cookie的关系:Session在创建时会依赖于Cookie,实质是Cookie存储一个SessionID作为每次提交服务器请求访问的Key,Session通过这个Key找到具体的Value值;

  (5)Application:

    ①基本概念:应用全局对象,被全局共享;使用操作之前先加Lock,完成之后UnLock;一般放在Global.asax中的Application_Start事件中;

     ②使用注意:很多书举例使用Application统计访问人数会导致网站在大并发量下会很十分卡;建议做网站开发尽量不用Application,也很少需要有用到它的时候;

PS:很多书中都会这样使用Application,是不是很眼熟?在使用前加Lock,完成之后UnLock虽然是一个比较好的同步操作,但是也正因为如此,加Lock会造成在大并发量的访问情况下网站系统出现卡顿的现象。

复制代码
 1     void Application_Start(object sender, EventArgs e)   
 2     {  
 3         // 在应用程序启动时运行的代码   
 4         Application["count"] = 0;//初始设置计数从0开始   
 5     }
 6 
 7     void Session_Start(object sender, EventArgs e)   
 8     {  
 9         // 在新会话启动时运行的代码   
10         Application.Lock();//同步,避免同时写入   
11         Application["count"]=(int)Application["count"]+1;//每建立一个会话该全局变量加1   
12         Application.UnLock();//同步结束   
13     }
复制代码

四、AJAX基础

  (1)AJAX产生原因:

    ①传统全局刷新导致用户体验不好;  ②IE5中首次引入了XMLHttpRequest;

  (2)AJAX基本概念:

    ①AJAX全称:AsynchronousJavascriptAndXML=异步的JavaScript和XML,一种进行页面局部刷新的技术;

     ②AJAX通过在后台与服务器进行少量数据交换,AJAX可以使网页实现异步更新,从而改善用户体验效果;

  (3)AJAX基本流程:

    ①浏览器HTML中使用JavaScript创建XMLHttpRequest → ②服务器端获取请求进行处理并返回符合AJAX风格的数据(例如Json) → ③浏览器JavaScript解析服务器返回的数据并局部显示或更改信息

  (4)AJAX核心对象:JavaScript对象XMLHttpRequest

    XmlHttpRequest使我们可以使用JavaScript向服务器提出请求并处理响应,而不阻塞用户。

PS:下面是一段经典的纯手工使用js对象XMLHttpRequest的实例:

复制代码
function ajax(url, onsuccess)
{
    var xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); //创建XMLHTTP对象,考虑兼容性。XHR
    xmlhttp.open("POST", url, true); //“准备”向服务器的GetDate1.ashx发出Post请求(GET可能会有缓存问题)。这里还没有发出请求

    //AJAX是异步的,并不是等到服务器端返回才继续执行
    xmlhttp.onreadystatechange = function ()
    {
        if (xmlhttp.readyState == 4) //readyState == 4 表示服务器返回完成数据了。之前可能会经历2(请求已发送,正在处理中)、3(响应中已有部分数据可用了,但是服务器还没有完成响应的生成)
        {
            if (xmlhttp.status == 200) //如果Http状态码为200则是成功
            {
                onsuccess(xmlhttp.responseText);
            }
            else
            {
                alert("AJAX服务器返回错误!");
            }
        }
    }
    //不要以为if (xmlhttp.readyState == 4) {在send之前执行!!!!
    xmlhttp.send(); //这时才开始发送请求。并不等于服务器端返回。请求发出去了,我不等!去监听onreadystatechange吧!
}
复制代码

  (5)AJAX优点缺点:

     ①优点:页面无刷新,在页面内与服务器通信,给用户的体验非常好;“按需取数据”,可以最大程度的减少冗余请求和响应对服务器造成的负担;基于XML标准化,并被浏览器广泛支持,不需安装插件等;

     ②缺点:由于AJAX只是局部刷新,所以页面的后退按钮是没有用的(破坏了后退按钮机制);对流媒体还有移动设备的支持不是太好;

五、客户端不可信

  (1)客户端验证不能代替服务端验证:

    ①客户端校验是为了更好的客户端体验,服务端校验是最后一次把关,防止恶意请求

    ②请求报文数据可以修改,例如Http报文中的UserAgent、Referer、Cookie等都是可以造假的;

    ③JQuery Validator+服务端校验是不错的开发方式;

  (2)ValidateRequest:

    ①ASP.Net默认对请求数据进行了校验->防止XSS攻击(跨站脚本攻击)

     ②对于要提交含HTML的文本内容需要关闭校验,在web.config中设置requestValidationMode="2.0"

    <system.web>
        <compilation debug="true" targetFramework="4.0" />
        <httpRuntime requestValidationMode="2.0" />
    </system.web>

    利用关闭校验的漏洞可以进行:送奖品的消息框、收集账号和密码;

  (3)CKEditor:经典的Web在线编辑器

ckeditor

    ①除了_samples、_source、*.php、*.asp都放到js/ckeditor文件夹下;

     ②页面中引用ckeditor.js;

     ③页面编辑器的位置使用textarea,在页面onload中或textarea之后使用CKEDITOR.replace(textarea);

总结思维导图

...
浏览 : 7728 阅读全文

一、ASP.Net的两种开发模式

1.1 ASP.Net WebForm的开发模式

webform

  (1)处理流程

  在传统的WebForm模式下,我们请求一个例如http://www.aspnetmvc.com/blog/index.aspx的URL,那么我们的WebForm程序会到网站根目录下去寻找blog目录下的index.aspx文件,然后由index.aspx页面的CodeBehind文件(.CS文件)进行逻辑处理,其中或许也包括到数据库去取出数据(其中的经过怎样的BLL到DAL这里就不谈了),然后再由index.aspx页面来呈现给用户。

  综上所述,在WebForm模式下:一个URL请求的是在服务器与该URL对应路径上的物理文件(ASPX文件或其他),然后由该文件来处理这个请求并返回结果给客户端。

  (2)开发方式

  • 服务器端控件
  • 一般处理程序+Html静态页+Ajax
  • 一般处理程序+Html模板引擎

1.2 ASP.Net MVC的开发模式

aspnet mvc

  (1)处理流程

  在ASP.Net MVC中,客户端所请求的URL是被映射到相应的Controller去,然后由Controller来处理业务逻辑,或许要从Model中取数据,然后再由Controller选择合适的View返回给客户端。再说回前面我们运行的ASP.NET MVC程序访问的http://www.aspnetmvc.com/Home/Index这个URL,它访问的其实是HomeController中的Index这个Action。

  (2)显著特点

  • 2009年第一个开源项目版本发布,至今已过去5年,发展逐渐完善
  • 更加简洁,更加接近原始的“请求-处理-响应”
  • 更加开发、更多的新的特点、社区活跃
  • 不会取代WebForm
  • 底层跟WebForm都是一样的,只是管道上不同的处理而已

二、MVC模式的两种不同解读

  MVC 模式两种理解:一种是表现模式,另外一种是架构模式。它将应用程序分成三个主要组件即:视图(View)、控制器(Controller)和模型(Model)。现在,我们来看看M-V-C分别代表什么?
  M:Model 主要是存储或者是处理数据的组件;Model其实是实现业务逻辑层对实体类相应数据库操作,如:CRUD。它包括数据、验证规则、数据访问和业务逻辑等应用程序信息。(补充:ViewModel:视图模型)

  V:View 是用户接口层组件。主要是将Model中的数据展示给用户。aspx和ascx文件被用来处理视图的职责;

  C: Controller 处理用户交互,从Model中获取数据并将数据传给指定的View;
  (1)MVC作为架构模式的理解
  作为架构模式时,View的职责就是负责展示数据,而Controller则负责获取View传递来的数据,然后调用业务逻辑层处理完成的数据传递给View进行展示。而Model则处理业务逻辑,并把结果返回给Controller。从传统三层架构上来看,View和Controller都属于UI层,而Model则横跨BLL与DAL层。
  (2)MVC作为表现模式的理解
  (3)MVC架构模式综合模型

  可以看出,对于MVC的两种不同理解的区别就在于对于Model的理解上:将Model作为业务模型(BLL、DAL等)还是作为视图模型(ViewModel)

三、WebForm vs MVC

comparation

  (1)WebForm

  优点: 1.提供了大量的服务器端控件,可以实现快速开发;

     2.ViewState回传数据很方便;

     3.学习成本低;

   缺点: 1. 封装太强,虽然学习成本低,很多底层东西让初学者不是很明白;

      2. 自定义控制不灵活,不利于美工和开发人员的配合,往往那些服务器控件处理稍有不慎就会导致出错;

      3. ViewState在页面中的传递会造成大量的流量消耗;

TIP:有关WebForm的服务器控件和ViewState的详细介绍,不了解的朋友可以阅读另一篇博文《ASP.Net WebForm学习笔记:aspx与服务器控件探秘》。

  (2)MVC

  优点: 1.很容易将复杂的应用分成Model(ViewModel)、View、Controller三个组件模型,将处理后台逻辑代码与前台展示逻辑进行了很好的分离,属于松耦合关系,在大项目应用中,更易于敏捷开发与测试驱动开发,有很强的可扩展性;

     2.因为没有服务器端控件,所以程序员控制的会更加灵活,页面更加干净,没有ViewState;

     3.通过修改路由规则,可以控制生成自定义的url,因此控制生成SEO友好的URL将更加容易;

     4.强类型View实现、Razor视图、Model绑定机制、Model的验证机制,更安全高效;

   缺点: 学习成本高,结构复杂,对未变化数据的不必要的频繁访问,也将损害操作性能。

四、第一个ASP.Net MVC程序

4.1 新建项目后的文件组织结构

  (1)新建一个ASP.Net MVC 4项目,选择“基本”配置与“ASPX”视图引擎(暂时不用Razor引擎)。

  (2)VS为我们生成的基本文件组织结构如下图所示:

  可以看出,VS默认帮我们创建好了Models、Views以及Controllers的三个文件夹,这三个文件夹就构成了我们的ASP.Net MVC模式的项目。其中,Controllers是所有控制器的类文件所在,而Models则是所有模型的类文件所在,而Views则是所有cshtml或aspx的文件所在。

4.2 控制器的“约定大于配置”

  在Controllers中新建一个控制器,取名为HomeController。在默认的Index这个Action中新建一个视图,默认名为Index即可。

  (1)Controller放到controllers文件夹中,并且命名方式以Controller结尾
  (2)每个Controller都对应View中的一个文件夹,文件夹的名称跟Controller名相同
  (3)Controller中的方法名都对应一个View视图(非必须,但是建议这么做)而且View的名字跟Action的名字相同
  (4)控制器必须是非静态类,并且要实现IController接口
  (5)Controller类型可以放到其他项目中

4.3 视图的相关约定

  (1)所有的视图必须放到Views目录下
  (2)不同控制器的视图用文件夹进行分割,每个控制器都对应一个视图目录
  (3)一般视图名字跟控制器的Action相对应(非必须)
  (4)多个控制器公共的视图放到Shared:例如公用的错误页、列表模板页、表单模板页等等;

4.4 数据传递的桥梁-ViewData与ViewBag

  首先,ViewData是一个Key/Value对的字典集合数据结构,用于在Controller和View之间构建起传递数据的桥梁。
  (1)ViewData是Controller的属性,此属性是继承ControllerBase而来
  (2)ViewPage下也有一个ViewData的一个属性
  (3)控制器的Action方法执行完成后,返回ViewResult,然后MVC框架在执行ExcuteResult方法时,Controller中的ViewData数据会传递给ViewPage类,其实就是把Controller的ViewData赋值给ViewPage页面的ViewData属性
  (4)ViewBag传递数据:我们对ViewBag的动态属性进行赋值,值实际上是存到了ViewData中,动态属性的名存成了ViewDataDictionary的键,动态属性的值存成了ViewDataDictionary的值。
PS:ViweBag其实是就一个包含了一层Dynamic的ViewData,两个兄弟共用的是一个容器。

  (5)ViewData与ViewBag的比较

ViewData ViewBag
它是Key/Value字典集合 它是dynamic类型对像
从Asp.net MVC 1 就有了 ASP.NET MVC3 才有
基于Asp.net 3.5 framework 基于Asp.net 4.0与.net framework
ViewData比ViewBag快 ViewBag比ViewData慢
在ViewPage中查询数据时需要转换合适的类型 在ViewPage中查询数据时不需要类型转换
有一些类型转换代码 可读性更好

  (6)如何在程序中使用ViewData与ViewBag

  ①在Controller中的代码

        public ActionResult Index()
        {
            ViewData["Name"] = "Edison Chou";
            ViewBag.Name = "Edison Chou";
            return View();
        }

  ②在View中的代码

复制代码
<body>
    <div>
        <h1>Hi,ASP.Net MVC First Demo!</h1>
        <%
            for (int i = 0; i < 5; i++)
            {
                Response.Write("Hello World!<br/>");
            }
        %>
        <p><%: ViewData["Name"] %></p>
        <p><%: ViewBag.Name %></p>
    </div>
</body>
复制代码

4.5 路由机制初步了解

  我们通过调试可以知道,在MVC中所有的请求都归结到控制器下面的Action。所以,所有的请求都是要指定一个具体的Action,Url的格式是根据路由规则来定的。那么,在ASP.Net MVC的路由规则默认又是什么,在哪里设置的呢?

复制代码
public class RouteConfig
{
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
}
复制代码

  打开App_Start文件夹,可以找到RouteConfig这个类,查看RouteConfig这个类的方法,可以知道原来是RegisterRoutes这个方法为我们的ASP.Net MVC项目设置了默认的路由规则:{controller}/{action}/{id},也就是说我们可以通过http://localhost/Home/Index/1这种URL来访问项目。如果我们想要改变默认的路由规则,例如我们想要以这种URL:http://localhost/Home-Index-1来访问项目,则直接将上面的默认路由规则改为:{controller}-{action}-{id}即可。

参考文章

(1)lulu Studio,《ASP.Net MVC入门:1-简介》,http://www.cnblogs.com/QLeelulu/archive/2008/09/30/1302462.html

(2)马伦,《ASP.Net MVC基础视频教程》,http://bbs.itcast.cn/thread-26722-1-1.html

(3)深山老林,《正确认识WebForm与ASP.Net MVC》,http://www.cnblogs.com/wlb/archive/2009/12/07/1618954.html

(4)zhaojunpeng,《ViewData和ViewBag的那些事》,http://blog.segmentfault.com/zhaojunpeng/1190000000472818

 

...
浏览 : 7716 阅读全文

一、一个功能强大的页面开发辅助类—HtmlHelper初步了解

1.1 有失必有得

  在ASP.Net MVC中微软并没有提供类似服务器端控件那种开发方式,毕竟微软的MVC就是传统的请求处理响应的回归。所以抛弃之前的那种事件响应的模型,抛弃服务器端控件也理所当然。

  但是,如果手写Html标签效率又比较低,可重用度比较低。这时,我们该怎样来提高效率呢?首先,经过上篇我们知道可以通过ViewData传递数据,于是我们可以写出以下的Html代码:

<input name="UserName" type="text" value="<%: ViewData["UserName"] %>" />

  虽然以上代码可以解决问题,但是效率还是比较低,特别是在列表集合项较多的时候,工作量会比较大。那么,还有木有一种更好的方式呢?别急,微软已经为我们想好了策略。微软为开发人员快速开发前台页面提供了丰富的HtmlHelper的辅助类,辅助我们快速开发前台页面,也提供了可扩展的接口,前台页面的标签可以可以做到高度可重用

1.2 HtmlHelper初窥

  我们可以通过在页面中通过Html.XXX来实现快速的Html标签编辑,并且可以方便地进行数据绑定。

<%: Html.Raw("<p>Hello,I am HtmlHelper!</p>") %>

  那么,为什么可以在页面中调用Html.XXX方法呢?通过ILSpy反编译ViewPage页,我们可以看到原来在ViewPage下有一个HtmlHelper类型的属性-Html。(这下终于知道,为什么可以在页面中使用Html.xxxx()了)

  那么这个HtmlHelper类又是一个什么类型的大神呢?继续反编译查看,在System.Web.Mvc命名空间下的HtmlHelper类型如下图所示,经过MSDN大神的讲解,HtmlHelper支持在视图中呈现 HTML 控件。那我们看看在此类中有木有传说中的TextBox、CheckBox的方法呢?经查看,木有。

  那么,我们为什么可以在页面中使用Html.TextBox()方法呢?这里就涉及到一个叫做“扩展方法”的东东了,HtmlHelper 类的扩展方法在 System.Web.Mvc.Html 命名空间中。 这些扩展添加了用于创建窗体、呈现 HTML 控件、呈现分部视图、执行输入验证等功能的帮助器方法。那么,有关如何自定义扩展方法请参阅本文第三部分,这里先卖个关子,暂不介绍。

1.3 为什么使用HtmlHelper?

   思考这样一个场景:我们的项目第一个版本中的路由规则是这样的{controller}/{action}/{id},于是我们项目中所有的<a>标签所指向的都是以刚刚的规则命名的href(例如:<a href='Home/User/1'></a>)。但是在第二版中,我们的路由规则也会变成了{controller}-{action}-{id},那么我们刚刚为超链接所设置的href便都无法正常访问了。这时,我们需要进行替换,单个替换或批量替换(例如改为:<a href='Home-User-1'></a>),虽然也可以解决问题,但是无疑增加了工作量,效率很低。

  那么,怎样来屏蔽这种变化所带来的不便呢?这里,通过使用HtmlHelper为我们提供的ActionLink标签,便可以解决这个问题。因为HtmlHelper是从服务器端自动帮你生成a标签,因此所生成的href会遵循目前的路由规则,也就帮我们屏蔽了变化,提高了工作效率。

二、没有服务器控件也能干大事—HtmlHelper重要方法介绍

  PS:这里的实例均没有加<% %>或@符号,要运行请自行加上。

  (1)ActionLink与RouteLink

复制代码
Html.ActionLink("这是一个连接", "Index", "Home")
带有QueryString的写法
Html.ActionLink("这是一个连接", "Index", "Home", new { page=1 },null)
Html.ActionLink("这是一个连接", "Index", new { page=1 })
有其它Html属性的写法
Html.ActionLink("这是一个连接", "Index", "Home", new { id="link1" })
Html.ActionLink("这是一个连接", "Index",null, new { id="link1" })
QueryString与Html属性同时存在
Html.ActionLink("这是一个连接", "Index", "Home", new { page = 1 }, new { id = "link1" })
Html.ActionLink("这是一个连接", "Index" , new { page = 1 }, new { id = "link1" })
复制代码

  其生成的结果为:

复制代码
<a href="/">这是一个连接</a>
带有QueryString的写法
<a href="/?page=1">这是一个连接</a>
<a href="/?page=1">这是一个连接</a>
有其它Html属性的写法
<a href="/?Length=4" id="link1">这是一个连接</a>
<a href="/" id="link1">这是一个连接</a>
QueryString与Html属性同时存在
<a href="/?page=1" id="link1">这是一个连接</a>
<a href="/?page=1" id="link1">这是一个连接</a>
复制代码

  RouteLink在用法几乎与ActionLink一致,这里就不再介绍,详情请参与MSDN;

  (2)TextBox与TextArea

  ①TextBox

Html.TextBox("input1") 
Html.TextBox("input2",Model.CategoryName,new{ @style = "width:300px;" }) 
Html.TextBox("input3", ViewData["Name"],new{ @style = "width:300px;" }) 
Html.TextBoxFor(a => a.CategoryName, new { @style = "width:300px;" })

  其生成的结果为:

<input id="input1" name="input1" type="text" value="" />
<input id="input2" name="input2" style="width:300px;" type="text" value="Beverages" />
<input id="input3" name="input3" style="width:300px;" type="text" value="" />
<input id="CategoryName" name="CategoryName" style="width:300px;" type="text" value="Electronic" />

  ②TextArea

Html.TextArea("input5", Model.CategoryName, 3, 9,null)
Html.TextAreaFor(a => a.CategoryName, 3, 3, null)

  其生成的结果为:

<textarea cols="9" id="input5" name="input5" rows="3">Electronic</textarea>
<textarea cols="3" id="CategoryName" name="CategoryName" rows="3">Electronic</textarea>

  这里可以看到,我们可以使用强类型来生成Html标签,例如:Html.TextBoxFor(a => a.CategoryName, new { @style = "width:300px;" }),这里的CategoryName就是某个类型的属性。

  (3)CheckBox

Html.CheckBox("chk1",true) 
Html.CheckBox("chk1", new { @class="checkBox"}) 
Html.CheckBoxFor(a =>a.IsVaild, new { @class = "checkBox" })

   其生成的结果为:

<input checked="checked" id="chk1" name="chk1" type="checkbox" value="true" /><input name="chk1" type="hidden" value="false" />
<input class="checkBox" id="chk1" name="chk1" type="checkbox" value="true" /><input name="chk1" type="hidden" value="false" />
<input checked="checked" class="checkBox" id="IsVaild" name="IsVaild" type="checkbox" value="true" /><input name="IsVaild" type="hidden" value="false" />

  (4)DropDownList

Html.DropDownList("ddl1", (SelectList)ViewData["Categories"],  "--Select One--")
Html.DropDownListFor(a => a.CategoryName, (SelectList)ViewData["Categories"], "--Select One--", new { @class = "dropdownlist" })

  其生成的结果为:

复制代码
<select id="ddl1" name="ddl1">
<option value="">--Select One--</option>
<option value="1">Beverages</option>
<option value="2">Condiments</option>
<option selected="selected" value="3">Confections</option>
<option value="4">Dairy Products</option>
<option value="5">Grains/Cereals</option>
<option value="6">Meat/Poultry</option>
<option value="7">Produce</option>
<option value="8">Seafood</option>
</select>
<select class="dropdownlist" id="CategoryName" name="CategoryName">
<option value="">--Select One--</option>
<option value="1">Beverages</option>
<option value="2">Condiments</option>
<option value="3">Confections</option>
<option value="4">Dairy Products</option>
<option value="5">Grains/Cereals</option>
<option value="6">Meat/Poultry</option>
<option value="7">Produce</option>
<option value="8">Seafood</option>
</select>
复制代码

  (5)RadioButton

<%: Html.RadioButton("Gender","1",true) %><%: Html.RadioButton("Gender","2",false) %>

  其生成的代码为:

<input checked="checked" id="Gender" name="Gender" type="radio" value="1" /><input id="Gender" name="Gender" type="radio" value="2" />

  (6)Encode与Raw

  Encode会将内容进行编码话,因此,如果你的内容中含有Html标签的话那么会被解析成特殊字符,例如:

<%: Html.Encode("<p>哈哈</p>") %>

  其生成的代码为:

&amp;lt;p&amp;gt;哈哈&amp;lt;/p&amp;gt;

  这里主要是为了防止XSS攻击和恶意脚本,因此在MVC中,默认的<%: %>就实现了<%: Html.Encode() %>。但是,某些时候如果我们需要输出Html或JavaScript内容的字符串,这时我们可以使用HtmlHelper为我们提供的其他方法。例如我们要输出刚刚那句话,我们可以如下使用:

<%: Html.Raw("<p>哈哈</p>") %>

  其生成的代码为:

<p>哈哈</p>

  在HtmlHelper中还提供了许多的扩展方法供我们方便创建Html,比如:BeginForm、EndForm等。关于其他的方法介绍,请自行搜索,这里不再一一赘述。

三、随时随地我也能扩展—HtmlHelper扩展方法简介

3.1 扩展方法简介

  借助MSDN的介绍:“扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。”扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用。我们可以回到第一部分对HtmlHelper的扩展类-InputExtension类上,它是对于HtmlHelper的扩展,那么怎么鉴别它是HtmlHelper的扩展呢?

3.2 扩展方法的三要素

  (1)静态类

  可以从上图看出,InputExtension首先是一个静态类;

  (2)静态方法

  既然是静态类,那么其所有的方法必然都是静态方法,例如:public static MvcHtmlString CheckBox();

  (3)this关键字

  可以从方法名定义中看出,第一个参数都是this HtmlHelper htmlHelper,代表对HtmlHelper类的扩展;

3.3 自定义扩展方法

  (1)在Models文件夹下新建一个类,取名为:MyHtmlHelperExt

  (2)将MyHtmlHelperExt设置为static,并写入以下的一个静态方法:

public static HtmlString MyExtHtmlLabel(this HtmlHelper helper, string value)
{
      return new HtmlString(string.Format("<span style='font-weight:bold;'>Hello-{0}-End</span>", value));
}

  (3)确定满足了扩展方法的三要素之后,将命名空间改为:System.Web.Mvc。

namespace System.Web.Mvc

PS:为什么要改命名空间为System.Web.Mvc?

这是因为如果不改命名空间,我们要使用自定义的扩展方法需要在每个页面中引入Models(MyHtmlHelper所在的那个命名空间)这个命名空间,为了防止重复的命名空间引入操作(想想我们使用Html.TextBox()不也没有引入命名空间么?),于是我们将命名空间与HtmlHelper类所在的命名空间保持一致。

  (4)在页面中我们就可以使用我们自己写的扩展方法了

<%: Html.MyExtHtmlLabel("EdisonChou") %>

  (5)查看页面效果

参考文章

(1)马伦,《ASP.Net MVC视频教程》,http://bbs.itcast.cn/thread-26722-1-1.html

(2)oer,《HtmlHelper使用大全》,http://www.cnblogs.com/oer2001/archive/2013/03/19/2968475.html

(3)MSDN,《扩展方法(C#编程指南)》,http://technet.microsoft.com/zh-cn/bb383977

(4)MSDN,《HtmlHelper类(System.Web.Mvc)》,http://msdn.microsoft.com/zh-cn/library/system.web.mvc.htmlhelper(v=vs.108).aspx

...
浏览 : 7690 阅读全文

一、天降神器“剃须刀” — Razor视图引擎

1.1 千呼万唤始出来的MVC3.0

  在MVC3.0版本的时候,微软终于引入了第二种模板引擎:Razor。在这之前,我们一直在使用WebForm时代沿留下来的ASPX引擎或者第三方的NVelocity模板引擎。

  Razor在减少代码冗余、增强代码可读性和Visual Studio智能感知方面,都有着突出的优势。Razor一经推出就深受广大ASP.Net开发者的喜爱。

1.2 Razor的语法

  (1)Razor文件类型:Razor支持两种文件类型,分别是.cshtml 和.vbhtml,其中.cshtml 的服务器代码使用了c#的语法,.vbhtml 的服务器代码使用了vb.net的语法。

  (2)@字符:@是Razor中的一个重要符号,它被定义为Razor服务器代码块的开始符号。例如,我们可以在View中直接写C#代码输出日期

1 <p>@DateTime.Now.ToString()</p>

1.3 Razor语句块

  (1)在Razor视图引擎中,我们可以使用@{code}来定义一段代码块

  (2)Razor支持代码混写:在代码块中插入HTML、在HTML中插入Razor语句都是可以的。例如,我们可以使用@来作for循环,还可以进行if判断

复制代码
@for (int i = 0; i < 10; i++)
{
     <p>@i</p>
}

@if (ViewData.Count > 0)
{
     <p>ViewData有数据</p>
     ViewData["Key"] = "Edison Chou";
}
else
{
     <p>ViewData暂无数据</p>
}
复制代码

1.4 Razor页面输出特殊字符串

  与在ASPX试图引擎中类似,如果要输出特殊字符串,还是借助HtmlHelper类提供的扩展方法来实现。

  (1)输出原生的字符串:@Html.Raw(html)
@Html.Raw("<h1>Razor</h1>")
  PS:默认的@会解析掉html代码
  (2)还可以通过使用HtmlString类型和MvcHtmlString类型字符串输出原生包含HTML的字符串
@{
            IHtmlString html = new HtmlString("<span style='color:red'>哈哈,我是Razor剃须刀!</span>");
            Response.Write(html);
}

1.5 Razor中的注释

  Razor服务器端注释为:@*  注释内容  *@

@*<p>你好,Razor引擎!</p>*@

1.6 Razor中转换数据类型

  在Razor中提供了很多方便我们进行数据类型转换的方法以及类型判断的方法,如下图所示:

  例如,我们可以在View中对一个字符串进行判断和转换:

@{
        string test = "Edison Chou";
        <p>@test.IsInt()</p>
        <p>@test.AsInt()</p>
}

二、Controller深入详解

2.1 控制器的三个职责 

  (1)处理跟用户的交互
  (2)处理业务逻辑的调用
  (3)指定具体的视图显示数据,并且把数据传递给视图

2.2 控制器的三个约定

  (1)必须是非静态类
  (2)必须实现IController接口
  (3)必须是以Controller结尾命名

2.3 无所不能的Action

  首先,在一个Controller中可以包含多个Action. 每一个Action都是一个方法, 返回一个ActionResult实例。那么,这个ActionResult是什么东东呢?

  由微软给出的注释可以知道,ActionResult是一个操作方法的结果,并且是一个抽象类,那么,也就代表了可以有多重结果的实现。这样就解释了,我们在Action中可以不仅可以返回ViewResult还可以返回JsonResult的原因。通过下表,我们可以清晰地看到,ActionResult的各种派生类的详情:

  从表中可以看出,我们所常用的各种XXXXResult都不约而同地继承了ActionResult这个基类,或者是其父类(例如:ViewResultBase)继承了ActionResult这个基类。因此,我们既可以在Action中返回视图,还可以返回文件流、重定向、空内容等结果。特别是,以前我们在WebForm时代常常与浏览器交互采用JSON格式的数据,需要使用JavaScriptSerializer这个类进行Serialize后返回。但是,在MVC的Action中,微软已经帮我们封装了好了JsonResult,因此,我们可以高兴地感慨:返回Json,So Easy!

2.4 ActionResult用法

  这里只介绍几个最常用的Result用法:

  (1)EmptyResult:当用户有误操作或者是图片防盗链的时候,这个EmptyResult就可以派上用场,返回它可以让用户啥也看不到内容,通过访问浏览器端的源代码,发现是一个空内容;

public ActionResult Empty()
{
      return new EmptyResult();
}

  (2)Content:通过Content可以向浏览器返回一段字符串类型的文本结果,就相当于Response.Write("xxxx");一样的效果;

public ActionResult ContentResultDemo()
{
      string contentString = "Hello Edison Chou!";
      return Content(contentString);
}

  (3)File:通过File可以向浏览器返回一段文件流,主要用于输出一些图片或文件提供下载等;

public ActionResult FileStreamResultDemo()
{
      FileStream fs = new FileStream(Server.MapPath(@"/Content/programmer.jpg"),
      FileMode.Open, FileAccess.Read);
      return File(fs, @"image/gif");
}

  (4)HttpUnauthorizedResult:通过HttpUnauthorizedResult可以向浏览器输出指定的状态码和状态提示,如果不指定状态码,则默认为401无权访问;

public ActionResult HttpUnauthorizedResultDemo()
{
     return new HttpUnauthorizedResult();
}

  (5)RedirectRedirectToAction:重定向与重定向到指定Action,我一般使用后者,主要是向浏览器发送HTTP 302的重定向响应;

复制代码
public ActionResult RedirectResultDemo()
{
      return Redirect(@"http://localhost:23531/Home/ContentResultDemo");
}

public ActionResult RedirectToRouteResultDemo()
{
      return RedirectToAction("FileStreamResultDemo", "Home");
}
复制代码

  (6)Json:通过Json可以轻松地将我们所需要返回的数据封装成为Json格式,进行Ajax开发可以变得so easy!

public ActionResult JsonResultDemo()
{
      var tempObj = new { Controller = "HomeController", Action = "JsonResultDemo" };
      return Json(tempObj, JsonRequestBehavior.AllowGet);
}

  (7)JavaScript:可以通过JavaScriptResult向浏览器单独输出一段JS代码,不过由于主流浏览器都对此进行了安全检查,因此你的JS代码也许无法正常执行,反而是会以字符串的形式显示在页面中;

三、Routing深入详解

  首先,ASP.Net MVC项目是URL请求驱动的,为什么访问localhost/home/index会传递给HomeController中名为index的action(即HomeController类中的index方法)?下面,我们一一来看下。

3.1 Routing的作用

  假如有一个请求:localhost/home/index,那么路由需要做的事情如下:

  (1)确定Controller

    (2)确定Action

  (3)确定其他参数

  (4)根据识别出来的数据,将请求传递给Controller和Action

3.2 神奇的路由规则

  根据路由的作用,我们可以知道它是一个“指路人”,指示我们的请求应该到达哪个Controller中的Action。那么,它是根据什么规则来指路的呢?我们可以在App_Start文件夹中的RouteConfig类中找到这个神奇的规则是如何制定的。

复制代码
public static void RegisterRoutes(RouteCollection routes)
{
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{id}",
          defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
      );
}
复制代码

  (1)首先,第一句routes.IgnoreRoute代表对所有axd的资源访问请求进行忽略,直接进行URL访问;这里可以阅读参考资料第(5)篇,了解其详细含义,这里就不再赘述;

  (2)然后,第二句开始使用MapRoute方法对整个网站定义了一个路由识别规则,这个规则的name是Default,url规则为:{controller}/{action}/{id}。例如我们要访问的URL为:localhost/home/index,在这个URL中,localhost是域名, 所以首先要去掉域名部分: home/index,也是就对应了上面代码中的这种URL结构: {controller}/{action}/{id}。正是因为我们建立了这种URL结构的识别规则,,所以能够识别出 Controller是home, action是index, id没有则为默认值""。

  (3)在MapRoute方法中为所有URL请求定义了一个defaults默认值:controller为空则指向Home,action为空则指向Index,而id则是可选的,非必须要的。

  这里,对于路由规则需要注意的有两点:

  (1)可以有多条路由规则;

  (2)路由规则是有顺序的(前面的规则被匹配后,后面的规则就不再匹配);

  我们可以在RegisterRoutes这个方法中添加一条自定义路由规则,并取名为Default2,具体规则代码如下:

routes.MapRoute(
     name: "Default2",
     url: "{controller}-{action}-{id}",
     defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

  这下如果我们以:localhost/Home-Index来访问时,我们原本想要的是根据Default2这个路由规则访问Home控制器下的Index这个Action,但却被告知以404提示:

  这是为什么呢?我们再来看看RegisterRoutes这个方法:

复制代码
public static void RegisterRoutes(RouteCollection routes)
{
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );

            routes.MapRoute(
                name: "Default2",
                url: "{controller}-{action}-{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
}
复制代码

  我们刚刚提到,路由规则是有顺序的(前面的规则被匹配后,后面的规则就不再匹配)。那么,可以推断,由于Default2在Default之后,有可能我们的请求localhost/Home-Index已经被Default这个规则所匹配了,因此Default2规则根本没有出场Show一下。那么,在Default规则中,它将Home-Index作为Controller的名字匹配,去访问Home-Index这个Controller,而Action使用默认的Index,那么它所请求的应该是这个URL:/localhost/Home-Index/Index。由于网站中,并没有Home-Index这个Controller,所以也就出现了刚刚那个404页面。

3.3 MapRoute方法介绍

  (1)MapRoute方法提供了以下几种方式的重载

  MapRoute( string name, string url);
  MapRoute( string name, string url, object defaults);
  MapRoute( string name, string url, string[] namespaces);
  MapRoute( string name, string url, object defaults, object constraints); MapRoute( string name, string url, object defaults, string[] namespaces);
  MapRoute( string name, string url, object defaults, object constraints, string[] namespaces);

  我们在上面所使用的便是第二种重载。

  (2)MapRoute方法参数详细介绍:

  ①name参数:

  规则名称, 可以随意起名。不可以重名,否则会发生错误: “路由集合中已经存在名为“Default”的路由。路由名必须是唯一的”。

  ②url参数:

  url获取数据的规则,这里不是正则表达式,将要识别的参数括起来即可,比如: {controller}/{action}

  最少只需要传递name和url参数就可以建立一条Routing(路由)规则,比如实例中的规则完全可以改为:

  routes.MapRoute( "Default", "{controller}/{action}");

  ③defaults参数:
  url参数的默认值:如果一个url只有controller: localhost/home/,而且我们只建立了一条url获取数据规则: {controller}/{action},那么这时就会为action参数设置defaults参数中规定的默认值。由于defaults参数是Object类型,所以可以传递一个匿名类型来初始化默认值:new { controller = "Home", action = "Index" }。
  在ASP.Net MVC网站默认实例中使用的是三个参数的MapRoute方法:
routes.MapRoute(
     name: "Default",
     url: "{controller}/{action}/{id}",
     defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
  ④constraints参数: 
  用来限定每个参数的规则或Http请求的类型。constraints属性是一个RouteValueDictionary对象,也就是一个字典表,但是这个字典表的值可以有两种类型:
  一是:用于定义正则表达式的字符串(正则表达式不区分大小写)。通过使用正则表达式可以规定参数格式,比如controller参数只能为4位数字:new { controller = @"\d{4}"}
routes.MapRoute(
     name: "Default2",
     url: "{controller}-{action}-{id}",
     defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
     constraints: new { controller = @"\d{4}" }
);
 
  二是:一个用于实现 IRouteConstraint 接口且包含Match方法的对象。

  例如:通过第IRouteConstraint 接口可以限制请求的类型(是GET还是POST)。因为System.Web.Routing中提供了HttpMethodConstraint类,,这个类实现了IRouteConstraint 接口。

  我们可以通过为RouteValueDictionary字典对象添加键为"httpMethod", 值为一个HttpMethodConstraint对象来为路由规则添加HTTP 谓词的限制,比如限制一条路由规则只能处理GET请求:httpMethod = new HttpMethodConstraint( "GET" )

复制代码
routes.MapRoute(
     name: "Default2",
     url: "{controller}-{action}-{id}",
     defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
     constraints: new { 
           controller = @"\d{4}", 
           httpMethod = new HttpMethodConstraint("GET") }
);
复制代码

3.4 URL路由实例详解

  一般来说,对于一个网站为了SEO友好,网址的URL层次最好不要超过三层:localhost/{频道}/{具体网页},其中域名第一层, 频道第二层, 那么最后的网页就只剩下最后一层了。如果使用默认实例中的“{controller}/{action}/{其他参数}"的形式则会影响网站的SEO。

  假设我们有一个综合型服务网站,其中有租房频道、酒店频道、KTV频道、电影院频道等等。我们应该怎样来设计URL路由规则呢?

  (1)首先,我们知道:可以有多条路由规则,但是路由规则是有顺序的(前面的规则被匹配后,后面的规则就不再匹配);所以,我们可以定义多条路由规则,粒度细的模块(比如:具体的酒店列表页面)路由规则放最前面,粒度粗的模块(比如:门户网站的首页)路由规则放在最后面。

  (2)其次,根据模块粒度划分层次结构,以粒度粗细排序为:网站首页->频道首页->具体内容;

  (3)最后,我们可以看一个具体的URL路由实例来分析一下:

复制代码
// 酒店列表页匹配
routes.MapRoute(
     "酒店列表页",
     "hotels/{action}-{city}-{price}-{star}",
     new { controller = "Hotel", action = "list", city = "beijing", price = "-1,-1", star = "-1" },
     new { city = @"[a-zA-Z]*", price = @"(\d)+\,(\d)+", star = "[-1-5]" }
);

// 酒店频道所有匹配 
routes.MapRoute(
     "酒店首页",
     "hotels/{*iiii}",
     new { controller = "Hotel", action = "default", hotelid = "" }
);
            
// 网站首页默认匹配
routes.MapRoute(
     "网站首页",
     "{*values}",
     new { controller = "Home", action = "index" }
);
复制代码

  (4)我们可以分析一下上面的路由规则所实现的功能:

   ①访问 www.mywebsite.com/hotels/list-chengdu-100,200-3 会访问酒店频道的列表页,并传入查询参数(price为100,200,star为3);

   ②访问 www.mywebsite.com/hotels 下面的任何其他页面地址,都会跳转到酒店首页;

   ③访问 www.mywebsite.com 下面的任何地址,如果未匹配上面2条,则跳转到首页;

  (5)根据上面的规则和实现的功能,我们可以做一个简单的总结如下:

   ①Routing规则有顺序(按照添加是的顺序),如果一个url匹配了多个路由规则,则按照第一个匹配的路由规则执行

   ②由于上面的规则,要将具体频道的具体页面放在最上方,将频道首页 和 网站首页 放在最下方。

   ③{*values}表示后面可以使用任意的格式。

3.5 URL路由调试

  在ASP.Net MVC中,默认是不允许对路由规则进行调试的。但是,我们可以通过使用RouteDebug来辅助进行调试。

  (1)首先,我们下载RouteDebug.dll到我们的项目中,并添加对其的引用。

  (2)其次,在Global.asax中的Application_Start方法中添加一句代码:

  RouteDeug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes); 

复制代码
protected void Application_Start()
{
     AreaRegistration.RegisterAllAreas();

     WebApiConfig.Register(GlobalConfiguration.Configuration);
     FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
     RouteConfig.RegisterRoutes(RouteTable.Routes);
     BundleConfig.RegisterBundles(BundleTable.Bundles);
     RouteDeug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);
}
复制代码

  (3)最后,F5调试运行,我们请求localhost/Home-Index这个URL时,可以清楚地发现,系统将Home-Index匹配了第一条默认路由规则,也就是将Home-Index作为Controller的名称进行匹配,这也就证明了为什么我们输入这个请求不会匹配第二条Default2的路由规则出现刚刚那个404页面了。

参考资料

  (1)马伦,《ASP.Net MVC视频教程》,http://bbs.itcast.cn/thread-26722-1-1.html

  (2)葡萄城控件技术团队,《ASP.NET MVC 5—控制器》,http://www.cnblogs.com/powertoolsteam/p/aspnet-mvc5-controller.html

  (3)李亮,《ASP.Net MVC3 Controller》,http://www.cnblogs.com/wlitsoft/archive/2012/05/28/2520799.html

  (4)顾里江,《ActionResult解析》,http://blog.csdn.net/gulijiang2008/article/details/7642213

  (5)紫鱼Tiler,《MVC路由中routes.IgnoreRoute》,http://www.cnblogs.com/flyfish2012/archive/2013/02/01/2889184.html

  (6)Capricornus,《路由匹配检测组件RouteDebug.dll》,http://www.cnblogs.com/Capricornus/archive/2010/08/26/1808907.html

...
浏览 : 7451 阅读全文

一、CASE的两种用法

1.1 等值判断->相当于switch case

  (1)具体用法模板:

    CASE expression

       WHEN value1 THEN returnvalue1

       WHEN value2 THEN returnvalue2

       WHEN value3 THEN returnvalue3

       ELSE defaultreturnvalue

    END 

  (2)具体使用示例:

  假设我们有一个论坛网站,其中有一张User表{ UId,Name,Level },Level是一个int类型,代表了用户等级类型,例如:1代表骨灰,2代表大虾等;我们就可以用CASE来对其进行等值判断了:

复制代码
select Name,Rank=(
    case Level
        when 1 then '骨灰'
        when 2 then '大虾'
        when 3 then '菜鸟'
    end
)    
from User
复制代码

1.2 条件判断->相当于if else if else

  (1)具体用法模板:

    CASE

      WHEN condition1 THEN returnvalue1

      WHEN condition2 THEN returnvalue2

      WHEN condition3 THEN returnvalue3

      ELSE defaultreturnvalue

    END

注意:then后面返回的数据类型要一致, returnvalue1、 returnvalue2、 returnvalue3的数据类型必须一致。

  (2)具体使用示例:

  假设我们有一张Score成绩表,里面记录有所有同学的成绩,此时我们想要对所有成绩进行一个评级,比如成绩如果>=90那么则评为A级,>=80且<90则评为B级,这里我们怎来写呢?

复制代码
select studentId,rank=(    
    case
        when english between 90 and 100 then 'A'
        when english between 80 and 89 then 'B'
        when english between 70 and 79 then 'C'
        when english between 60 and 69 then 'D'
        when english < 60 then 'E'
        else '缺考'
    end
)
from Score
复制代码

二、子查询的用法

2.1 子查询初步

  就像使用普通的表一样,被当作结果集的查询语句被称为子查询。所有可以使用表的地方几乎都可以使用子查询来代替。例如:我们如果要找到所有计科一班的同学信息,可以首先通过T_Class表找到计科一班的Id,然后再在T_Student表中找到所有ClassId为计科一班Id的行即可。

select * from T_Student where ClassId=
(
    select Id from T_Class where Name='计科一班'
)

2.2 单值子查询

  只有返回且仅返回一行、一列数据的子查询才能当成单值子查询。例如我们上面提到的例子,子查询中只返回了一个ClassId,这就是单值子查询。当子查询跟随在=、!=、<、<=、>、>=,<> 之后,或子查询用作表达式,只能使用单值子查询。

2.3 多值子查询

  如果子查询是多行单列的子查询,这样的子查询的结果集其实是一个集合,那么可以使用in关键字代替=号。例如:我们如果想快速地在T_Student表中删除计科一班和计科二班的所有学生记录,我们可以使用in关键字:

delete from T_Student where ClassId in
(
    select Id from T_Class where Name='计科一班' or Name='计科二班'
)

2.4 Exists—你存在我深深的脑海里

  exists是用来判断是否存在的,当exists查询中的查询存在结果时则返回真,否则返回假。not exists则相反。

  exists做为where 条件时,是先对where 前的主查询询进行查询,然后用主查询的结果一个一个的代入exists的查询进行判断,如果为真则输出当前这一条主查询的结果,否则不输出。

  exists后面的查询称为相关子查询,即子查询的查询条件依赖于外层父查询中的某个属性值,其处理过程一般为:先取外层查询中的第一个元组,根据它与内层查询中的相关属性值处理内层查询,若where子句返回true,则将此元组放入结果表中,然后取外层查询中的下一个元组,重复这个过程直到全部检查完毕为止。

  例如:我们有一张人员信息表,里边有一个人员类型Id字段(pTypeId),它是一个外键,对应着人员类型表的主键ptId。如果我们有以下的SQL语句,使用Exists关键字则可以有如下的理解:

select * from Employee e where exists 
(select * from EmployeeType et where e.pTypeId=et.ptId)

  那么,在这句SQL的执行过程中,我们可以将其理解为一个双重的for循环,外边是主表的循环遍历,然后将其放到一个temp变量中,再进入从表的for循环,并与从表的项进行一个一个的按照匹配规则(这里是e.pTypeId=et.ptId)进行匹配,如果有匹配成功则返回true,并且将这一行记录放到要返回的结果集中,否则返回false。

三、手写分页SQL代码

  这里假设每页的页大小为10条记录

3.1 利用Top N进行简单分页

  (1)如果我们要获取第一页的数据,也就是前10个:

select top 10 * from Account

  (2)现在我们要获取第一页之后的数据,也就是第20个~最后一个:

select * from Account where Id not in (select top 10 Id from Account) 

  (3)现在我们对第20个~最后一个的数据集中取前10个也就成为第二页的数据了:

select top 10 * from Account where Id not in (select top 10 Id from Account) 

  (4)将上述代码总结为分页代码,设页大小为pageSize,请求页号为pageIndex:

select top @pageSize * from Account where Id not in (select top ((@pageIndex-1)*@pageSize) Id from Account) 

PS:这种分页方式的缺点是如果要取很多页之后的数据,那么就要取出前面很多页的ID,查询开销较大,执行效率也就低下。

  从图中可以看出,在小数据量的对比下,Top N的查询开销较小。但是在大数据量的对比下,Row_Number的方式会取得更高的查询效率以及较小的开销。

3.2 利用Row_Number()进行高效分页

  (1)SQL Server 2005后增加了Row_Number函数,可以简化分页代码的实现。首先,Row_Number()是一个排序函数,它可以生成一个有序的行号(如果单靠ID来排序,中间存在断层,例如某一个ID行已经被删除了)。根据MSDN的定义:返回结果集分区内行的序列号,每个分区的第一行从 1 开始。而排序的标准是什么呢?这个就要靠紧跟其后的OVER()语句来定义了。这里我们可以通过一个示例来看看,其生成的行号如何。

select *,ROW_NUMBER() OVER(order by Id) as RowNum from Account

  (2)根据ROW_NUMBER()的使用,我们可以将其应用到分页上,于是我们可以写出以下的代码来实现获取第二页的数据集:

select * from (
    select *,ROW_NUMBER() OVER(Order by Id) as rownum from Account
) as t
where t.rownum between 11 and 20
order by t.Id asc

  (3)将上述代码总结为分页代码,设页大小为pageSize,请求页号为pageIndex:

select * from (
    select *,ROW_NUMBER() OVER(Order by Id) as rownum from Account
) as t
where t.rownum between (@pageIndex-1)*pageSize+1 and @pageSize*pageIndex
order by t.Id asc

四、各种连接—JOIN

4.1 Join==Inner Join

  默认情况下,使用Join则代表Inner Join内连接,表示两个表根据某种等值规则进行连接。例如下面示例:查询所有学生的学号、姓名及所在班级

select p.Id,p.Name,c.Name from T_Person p join T_Class c on p.ClassId=c.Id

4.2 Left Join

  例如:查询所有学生(参加及未参加考试的都算)及成绩,这里涉及到学生表及成绩表,题目要求参加及未参加考试的都要列出来,于是以学生表为基准,对成绩表进行左连接:

select * from Student s
left join SC sc on s.S#=sc.S#

  可以通过运行结果图,看到王二小这个童鞋没有参加考试,也就没有成绩。

4.3 Right Join

  例如:要查询出所有没有参加考试(在成绩表中不存在的学生)的学生的姓名。于是还是可以以学生表为基准,但是要对成绩表进行右连接:

select * from Student s
right join SC sc on s.S#=sc.S#

4.4 Cross Join

  此种连接在实际应用中不算常见的,但却是理论基础,因为它代表了笛卡尔积。其实,所有连接方式都会先生成临时笛卡尔积表,笛卡尔积是关系代数里的一个概念,表示第一个表的行数乘以第二个表的行数等于笛卡尔积结果集的大小。

select * from Student s
cross join SC sc

  这里假如Student表中有10行记录,SC表中有20行记录,那么两表进行交叉连接笛卡尔积运算会得到10*20=200行记录的结果集。

五、视图

5.1 三个角度看视图

  从用户角度来看,一个视图是从一个特定的角度来查看数据库中的数据。

  从数据库系统内部来看,一个视图是由SELECT语句组成的查询定义的虚拟表

  从数据库系统内部来看,视图是由一张或多张表中的数据组成的;从数据库系统外部来看,视图就如同一张表一样,对表能够进行的一般操作都可以应用于视图,例如查询,插入,修改,删除操作等。

5.2 创建视图

  例如,我们可以创建一个学生成绩详细信息视图,对一个需要进行三表连接的查询进行封装:

create view vw_sc
as
select s.S#,s.Sname,c.Cname,sc.Score from Student s
join SC sc on s.S#=sc.S#
join Course c on sc.C#=c.C#

  然后,我们对vw_sc进行select查询:

5.3 视图的注意事项

  (1)视图在操作上和数据表没有什么区别,但两者的差异是其本质是不同:数据表是实际存储记录的地方,然而视图并不保存任何记录

  (2)相同的数据表,根据不同用户的不同需求,可以创建不同的视图(不同的查询语句)。

  (3)视图的目的是方便查询,所以一般情况下不对视图进行增改,不能删
...
浏览 : 7127 阅读全文

一、校验 — 表单不是你想提想提就能提

1.1 DataAnnotations(数据注解)

  位于 System.ComponentModel.DataAnnotations 命名空间中的特性指定对数据模型中的各个字段的验证。这些特性用于定义常见的验证模式,例如范围检查和必填字段。而 DataAnnotations 特性使 MVC 能够提供客户端和服务器验证检查,使你无需进行额外的编码来控制数据的有效

  通过为模型类增加数据描述的 DataAnnotations ,我们可以容易地为应用程序增加验证的功能。DataAnnotations 允许我们描述希望应用在模型属性上的验证规则,ASP.NET MVC 将会使用这些 DataAnnotations ,然后将适当的验证信息返回给用户。

  在DataAnnotations为我们所提供的众多内置验证特性中,用的最多的其中的四个是:

  (0)[DisplayName]:显示名 – 定义表单字段的提示名称

  (1)[Required] :必须 – 表示这个属性是必须提供内容的字段

  (2)[StringLength]:字符串长度 – 定义字符串类型的属性的最大长度

  (3)[Range]:范围 – 为数字类型的属性提供最大值和最小值

  (4)[RegularExpression]:正则表达式 – 指定动态数据中的数据字段值必须与指定的正则表达式匹配 

1.2 使用DataAnnotations为Model进行校验

  假设我们的Model中有一个UserInfo的实体,其定义如下:

    public class UserInfo
    {
        public int Id { get; set; }
        public string UserName { get; set; }
        public int Age { get; set; }
    }

  UserInfo的属性很简单,只有三个:Id,UserName和Age三个字段;现在我们可以为其增加验证特性,看看其为我们提供的强大的校验功能。

  (1)非空验证

  添加特性:

复制代码
[Display(Name="用户名")]
[Required(ErrorMessage = "*姓名必填")]
public string UserName { get; set; }
    
[Display(Name = "年龄")]
[Required(ErrorMessage = "*年龄必填")]
public int Age { get; set; }
复制代码

  验证效果:

  (2)字符串长度验证

  添加特性:

[Display(Name="用户名")]
[Required(ErrorMessage = "*姓名必填")]
[StringLength(5, ErrorMessage = "*长度必须小于5")]
public string UserName { get; set; }

  验证效果:

  (3)范围验证

  添加特性:

[Display(Name = "年龄")]
[Required(ErrorMessage = "*年龄必填")]
[Range(18, 120)]
public int Age { get; set; }

  验证效果:

  (4)正则表达式验证

  添加特性:验证用户输入的是否是数字,正则表达式匹配

[Display(Name = "年龄")]
[Required(ErrorMessage = "*年龄必填")]
[Range(18, 120)]
[RegularExpression(@"^\d+$", ErrorMessage = "*请输入合法数字")]
public int Age { get; set; }

  验证效果:

  (5)浏览所生成的HTML代码

  从上图可以看出,我们在浏览器端的校验都是通过为html标签设置自定义属性来实现的,我们在Model中为其添加的各种校验特性,都会在客户端生成一个特定的属性,例如:data-val-length-max=“5”与data-val-length="*长度必须小于5"对应[StringLength(5, ErrorMessage = "*长度必须小于5")]。然后,通过jquery validate在客户端每次提交之前进行校验,如果校验匹配中有不符合规则的,则将message显示在一个特定的span标签(class="field-validation-valid")内,并阻止此次表单提交操作。

1.3 使用DataAnnotations的注意事项

  (1)首先,要确保需要进行校验的页面中引入了指定的几个js文件:

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

  当然,jquery库的js文件也是必须的,而且在上面这两个js之前引入;

  (2)在 Web.config 的appSettings中,已经默认支持了客户端验证(MVC3.0及更高版本中默认支持,MVC2.0则需要修改一下):

<!-- 是否启用全局客户端校验 -->
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />

PS:Unobtrusive Javascript有三层含义:

一是在HTML代码中不会随意的插入Javsscript代码,只在标签中加一些额外的属性值,然后被引用的脚本文件识别和处理;

二是通过脚本文件所增加的功能是一种渐进式的增强,当客户端不支持或禁用了Javsscript时网页所提供的功能仍然能够实现,只是用户体验会降低;

三是能够兼容不同的浏览器。

  (3)在Action中如果要对客户端是否通过了校验进行验证,可以通过以下代码实现:

复制代码
        [HttpPost]
        public ActionResult Add(UserInfo userInfo)
        {
            if (ModelState.IsValid)
            {
                 // To do fun
            }

            return RedirectToAction("Index");
        }
复制代码

  如果通过校验,则ModelState的IsValid属性(bool类型)会变为true,反之则为false。

二、ASP.Net MVC下的两种AJAX方式

2.1 使用JQuery AJAX方式

  首先,在ASP.Net MVC中使用此种方式跟普通的WebForm的开发方式是一致的,需要注意的是:Url地址不同->请求的是Controller下的Action,例如在WebForm中请求的url通常是/Ajax/UserHandler.ashx,而在MVC中请求的url通常为:/User/GetAll。

  例如,我们在一个View中添加一个按钮,用于使用AJAX获取一个服务器端的时间:

<h1>JQuery Ajax方式</h1>
<input id="btnJQuery" type="button" value="获取服务器时间" />

  在Home控制器中增加一个用于返回时间的Action:

public ActionResult GetServerDate()
{return Content(DateTime.Now.ToString());
}

  在View中增加一段JQuery代码,为btnJQuery按钮绑定一个Click事件:

复制代码
    $(function () {
            $("#btnJQuery").click(function () {
                $.post("/Home/GetServerDate", {}, function (data) {
                    if (data != null) {
                        $("#spTime").html(data);
                    }
                });
            });
    });
复制代码

  这里通过JQuery AJAX发送一个异步的POST请求,获取服务器时间结果,并将其显示在span标签内:

  至此,一个使用JQuery Ajax的MVC页面就完成了。但是,这仅是一个最简单的AJAX示例,在实际开发中往往比较复杂一点。

  需要注意的是:

  (1)如果你在JQuery AJAX中使用的是get方式的提交,那么在在使用Json返回JsonResult时注意要将第二个参数设置允许Get提交方式:return Json("",JsonRequestBehavior.AllowGet),否则你用get方式是无权执行要请求的Action方法的。

  (2)在Ajax开发中要注意Ajax方法体内的参数设置正确,特别是参数名要和Action中的参数名保持一致;

  (3)如果在Action中为其设置了[HttpPost]或[HttpGet],那么提交方式要跟Action打的标签一致;

2.2 使用Microsoft AJAX方式

  在ASP.Net MVC中除了可以使用JQuery AJAX外,Microsoft为我们提供了另一套实用且更简单的AJAX方案,我们姑且称其为:Microsoft AJAX方式。

  (1)首先:

  需要将微软提供的js脚本引入到页面中:其实就是jquery.unobtrusive-ajax.js

<script src="~/Scripts/jquery-1.7.1.min.js"></script>
<script src="~/Scripts/jquery.unobtrusive-ajax.min.js"></script>

  确保在Web.config中启用了Unobtrusive JavaScript

<add key="UnobtrusiveJavaScriptEnabled" value="true" />

  (2)其次,使用Ajax.BeginForm方法构造一个form表单:

复制代码
       <h1>Microsoft Ajax方式</h1>
        @using (Ajax.BeginForm("GetServerDate", "Home", new AjaxOptions()
        {
            HttpMethod = "POST",
            Confirm = "您确定要提交?",
            InsertionMode = InsertionMode.Replace,
            UpdateTargetId = "spResult",
            OnSuccess = "afterSuccess",
            LoadingElementId="loading"
        }))
        {
            <table>
                <tr>
                    <td>用户名:</td>
                    <td>
                        <input id="txtUserName" name="UserName" /></td>
                </tr>
                <tr>
                    <td>密   码:</td>
                    <td>
                        <input id="txtPassword" name="Password" /></td>
                </tr>
                <tr>
                    <td align="center" colspan="2">
                        <input id="btnAjax" type="submit" value="提 交" />
                    </td>
                </tr>
                <tr>
                    <td align="center" colspan="2">
                        <div id="loading" style="display:none">
                            <img style="vertical-align:middle" src="~/Content/ico_loading2.gif" />正在获取中,请稍候...
                        </div>
                        <span id="spResult"></span>
                    </td>
                </tr>
            </table>
        } 
复制代码

  这里需要注意的是:

  ①Ajax.BeginForm没有提供闭合的方法,需要使用Using配合关闭;

  ②AjaxOptions参数的设置:

  HttpMethod代表此次AJAX请求到底是POST方式还是GET方式?这里是POST方式;

  Confirm代表点击提交按钮后提出的确认对话框,并给出用户给定的提示语,这里是:您确定要提交?

  InsertionMode代表请求获得后的数据是要替换还是追加,一般选择替换,即Replace; 

  UpdateTargetId代表需要替换的div标签的Id,这里是一个span标签,代表需要显示的信息都显示在这个span内;

  OnSuccess代表请求成功后所需要执行的回调方法,是一个js方法,可以自定义,这里是一个function afterSuccess()的方法;

        function afterSuccess(data) {
            //alert("您已成功获取数据:" + data);
        }

  LoadingElementId="loading"是一个很有意思的属性,代表在ajax请求期间为了提供良好的用户体验,可以给出一个正在加载中的提示,而这个LoadingElementId则代表一个提示的div区域的Id。这里主要是指id为loading的这个div,其中有一张gif图片及一句话:正在获取中,请稍等...的提示。

<div id="loading" style="display:none">
      <img style="vertical-align:middle" src="~/Content/ico_loading2.gif" />正在获取中,请稍候...
</div>

  为了显示加载提示的效果,我们人为地修改一下Action方法,使用Thread.Sleep(3000)来延迟一下请求返回时间

        public ActionResult GetServerDate()
        {
            System.Threading.Thread.Sleep(3000);
            return Content(DateTime.Now.ToString());
        }

  好了,现在我们可以看一下效果如何:

  到此,我们的Microsoft AJAX就算完成了一个最简单的Demo了。那么,我们不禁想知道Microsoft AJAX是怎么做到的?跟校验一样,我们浏览一下生成的form表单就知道了:

  原来我们在AjaxOptions中所设置的参数也被解析成了form的自定义属性,它们的对应关系如下:

三、为AOP而生 — ASP.Net MVC默认的过滤器

3.1 过滤器初步

  大一点的项目总会有相关的AOP面向切面的组件,而MVC(特指:Asp.Net MVC,以下皆同)项目中Action在执行前或者执行后我们想做一些特殊的操作(比如身份验证,日志,异常,行为截取等),而不想让MVC开发人员去关心和写这部分重复的代码。那么,我们可以通过AOP截取实现,而在MVC项目中我们就可以直接使用它提供的Filter的特性帮我们解决,不用自己实现复杂的AOP了。
AOP:Aspect Oriented Programming(AOP)是较为热门的一个话题。AOP,国内大致译作“面向切面编程”。针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
  利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等。

3.2 微软提供的几种默认过滤器

  微软默认为我们提供了四种类型的过滤器(Filter),如下图所示:

  这里,我们主要来看看ActionFilter(Action过滤器)和ExceptionFilter(异常过滤器)的使用:

  (1)Action Filter

  ActionFilterAttribute默认实现了IActionFilter和IResultFilter。而ActionFilterAttribute是一个Abstract的类型,所以不能直接使用,因为它不能实例化,所以我们想使用它必须继承一下它然后才能使用。

  ①因此,我们首先在Models中新建一个类,取名为:MyActionFilterAttribute(以Attribute结尾比较符合编码规范),并使其继承自ActionFilterAttribute,然后重写基类所提供的虚方法:

复制代码
    public class MyActionFilterAttribute : ActionFilterAttribute
    {
        public string Name { get; set; }

        /// <summary>
        /// Action 执行之前先执行此方法
        /// </summary>
        /// <param name="filterContext">过滤器上下文</param>
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            base.OnActionExecuting(filterContext);
            HttpContext.Current.Response.Write("<br />OnActionExecuting :" + Name);
        }

        /// <summary>
        /// Action执行之后
        /// </summary>
        /// <param name="filterContext">过滤器上下文</param>
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            base.OnActionExecuted(filterContext);
            HttpContext.Current.Response.Write("<br />OnActionExecuted :" + Name);
        }

        /// <summary>
        /// ActionResult执行之前先执行此方法
        /// </summary>
        /// <param name="filterContext">过滤器上下文</param>
        public override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            base.OnResultExecuting(filterContext);
            HttpContext.Current.Response.Write("<br />OnResultExecuting :" + Name);

        }

        /// <summary>
        /// ActionResult执行之后先执行此方法
        /// </summary>
        /// <param name="filterContext">过滤器上下文</param>
        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            base.OnResultExecuted(filterContext);
            HttpContext.Current.Response.Write("<br />OnResultExecuted :" + Name);
        }
    }
复制代码

  这里我们重写了四个虚方法,他们各自代表了在Action执行之前和之后需要执行的业务逻辑,以及在Result执行之前和之后需要执行的业务逻辑。这里的Result主要是指我们在Action中进行return 语句返回结果时(例如:return Content("Hello Filter!");),之前和之后要执行的逻辑处理。

  比如:我们想要在每个Action执行之前进行用户是否登录的校验,可以在OnActionExecuting中判断用户Session是否存在,如果存在则继续执行Action的具体业务代码,如果不存在则重定向页面到登陆页,后边的Action业务代码不再执行。

  ②现在有了自定义的过滤器,我们怎么将其应用到Action中呢?这里有三种方式:

  一是给某个控制器的某个Action指定此Filter:

        [MyActionFilter(Name = "Filter Action")]
        public ActionResult Filter()
        {
            Response.Write("<p>Action正在努力执行中...</p>");
            return Content("<p>OK:视图成功被渲染</p>");
        }

  二是给某个控制器的所有Action指定此Filter:

    [MyActionFilter(Name="Home Filter")]
    public class HomeController : Controller
    {
    }

  但是,要注意的是:如果既给Controller指定了Filter,又给该Controller中的某个Action指定了Filter,那么具体的这个Action以离其定义最近的Filter为准,也就是一个优先级的顺序问题:Action的Filter优先级高于Controller的Filter。

  三是给此项目中的所有控制器即全局指定此Filter:在App_Start中更改FilterConfig类,此种方式优先级最低

        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            // 注册自定义Action过滤器:优先级最低,但是可以作用到所有的控制器和Action
            filters.Add(new MyActionFilterAttribute() { Name = "Global Controller" });
        }

  ③现在我们来看看具体的效果:

  可以看到,我们的/Home/Filter这个Action中只有两句代码,一句Response.Write,另一句是return Content();在Response.Write之前执行了OnActionExecuting的过滤器方法,之后则执行了OnActionExecuted的过滤器方法;我们刚刚说了,在Action中的return语句代表了Result,那么在Result之前执行了OnResultExecuting过滤器方法,之后则执行了OnResultExecuted过滤器方法。这里仅仅是为了展示,在实际开发中是需要写一些具体的业务逻辑处理的,例如:判断用户的登录状态,记录用户的操作日志等等。

  (2)Exception Filter

  ①同样,在Models中新建一个类,取名为:MyExceptionFilterAttribute,并使其继承自HandleErrorAttribute。

复制代码
    public class MyExceptionFilterAttribute : HandleErrorAttribute
    {
        public override void OnException(ExceptionContext filterContext)
        {
            base.OnException(filterContext);

            HttpContext.Current.Response.Redirect("/Home/Index");
        }
    }
复制代码

  这里,重写基类的OnException方法,这里仅仅为了演示效果,没有对异常进行处理。在实际开发中,需要获取异常对象,并将其记录至日志中。例如,下面一段代码:

复制代码
     public override void OnException(ExceptionContext filterContext)
        {
            base.OnException(filterContext);
            //获取系统异常消息记录
            string strException = filterContext.Exception.Message;
            if (!string.IsNullOrEmpty(strException))
            {
                //使用Log4Net记录异常信息
                Exception exception = filterContext.Exception;
                if (exception != null)
                {
                    LogHelper.WriteErrorLog(strException, exception);
                }
                else
                {
                    LogHelper.WriteErrorLog(strException);
                }
            }
            
       filterContext.HttpContext.Response.Redirect("~/GlobalErrorPage.html");
        }
复制代码

  ②有了异常过滤器,我们怎么来应用到项目中呢?答案也在App_Start中,还是在FilterConfig类中,新添一句代码进行注册:

复制代码
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            // 注册自定义Action过滤器:优先级最低,但是可以作用到所有的控制器和Action
            filters.Add(new MyActionFilterAttribute() { Name = "Global Controller" });
            // 注册自定义Exception过滤器
            filters.Add(new MyExceptionFilterAttribute());
        }
    }
复制代码

  ③为了测试,我们新增一个Action,使其能够出现一个异常:DividedByZero

复制代码
        public ActionResult Exception()
        {
            int a = 10;
            int b = 0;
            int c = a / b;
            return Content("Exception is happened.");
        }
复制代码

  ④当我们测试这个Action时,会发现系统执行了自定义的异常过滤器,将我们的这个请求改为重定向到Index这个Action了。

参考资料

(1)蒋金楠,《ASP.NET MVC下的四种验证编程方式》,http://www.cnblogs.com/artech/p/asp-net-mvc-validation-programming.html

(2)蒋金楠,《ASP.NET MVC下的四种验证编程方式[续篇]》,http://www.cnblogs.com/artech/p/asp-net-mvc-4-validation.html

(3)马伦,《ASP.NET MVC 2014特供教程》,http://bbs.itcast.cn/thread-26722-1-1.html

(4)w809026418,《MVC中使用 DataAnnotations 进行模型验证》,http://www.cnblogs.com/haogj/archive/2011/11/16/2251920.html

(5)刘俊峰,《ASP.NET MVC中Unobtrusive Ajax的妙用》,http://www.cnblogs.com/rufi/archive/2012/03/31/unobtrusive-ajax.html

 

...
浏览 : 8068 阅读全文