最近新建 asp.net core mvc 项目的时候不小心选错了个模板,发现了一种新的项目模板。它使用cshtml视图模板,但是没有controller文件夹。后来才发现这是asp.net core框架新推出的razor pages技术。

什么是razor pages

“razor pages 使编码更加简单更加富有生产力”这是微软说的==!。razor pages 简化了传统的mvc模式,仅仅使用视图跟模型来完成网页的渲染跟业务逻辑的处理。模型里包含了数据跟方法,通过绑定技术跟视图建立联系,这就有点像服务端的绑定技术。下面使用一个标准的crud示例来演示razor pages的开发,并且简单的探索一下它是如何工作的。

新建razor pages项目

在visual studio中新建razor pages项目。

项目结构

新建项目的目录结构比mvc项目简单。它没有controllers目录,pages有点像mvc项目的views目录,里面存放了cshtml模板。随便点开一个cshtml文件,发现它都包含了一个cs文件。这是跟mvc项目最大的不同,这个结构让人回忆起那古老的webform技术,o(╥﹏╥)o 。

新建razor page

我们模拟开发一个学生管理系统。一共包含4个页面:列表页面、新增页面、修改页面、删除页面。首先我们新建一个列表页面。
在pages目录下面新建student目录。在student目录下新建4个razor page名叫:list、add、update、delete。

建好后目录结构是这样:

模拟数据访问仓储

由于这是个演示项目,所以我们使用静态变量来简单模拟下数据持久。
在项目下新建一个data目录,在目录下新建student实体类:

    public class student
    {
        public int id { get; set; }
        public string name { get; set; }

        public string class { get; set; }

        public int age { get; set; }

        public string sex { get; set; }
    }

在data目录下新建istudentrepository跟studentrepository类:

    public interface istudentrepository
    {
        list<student> list();

        student get(int id);

        bool add(student student);

        bool update(student student);

        bool delete(int id);
    }

    public class studentrepository : istudentrepository
    {
        private static list<student> students = new list<student> {
                new student{ id=1, name="小红", age=10, class="1班", sex="女"},
                new student{ id=2, name="小明", age=11, class="2班", sex="男"},
                new student{ id=3, name="小强", age=12, class="3班", sex="男"}
        };

        public bool add(student student)
        {
            students.add(student);

            return true;
        }

        public bool delete(int id)
        {
            var stu = students.firstordefault(s => s.id == id);
            if (stu != null)
            {
                students.remove(stu);
            }

            return true;
        }

        public student get(int id)
        {
            return students.firstordefault(s=>s.id == id);
        }

        public list<student> list()
        {
            return students;
        }

        public bool update(student student)
        {
            var stu = students.firstordefault(s=>s.id == student.id);
            if (stu != null)
            {
                students.remove(stu);
            }

            students.add(student);
            return true;
        }
    }

我们新建了一个irepository接口,里面有几个基本的crud的方法。然后新建一个实现类,并且使用静态变量保存数据,模拟数据持久化。
当然还得在di容器中注册一下:

  public void configureservices(iservicecollection services)
        {
            services.addrazorpages();
            //注册repository
            services.addscoped<istudentrepository, studentrepository>();
        }

实现列表(student/list)页面

列表页面用来展现所有的学生信息。
修改listmodel类:

    public class listmodel : pagemodel
    {
        private readonly istudentrepository _studentrepository;
        public list<student> students { get; set; }
        public listmodel(istudentrepository studentrepository) 
        {
            _studentrepository = studentrepository;
        }

        public void onget()
        {
            students = _studentrepository.list();
        }
    }

修改list.cshtml模板:

@page
@model razorpagecrud.listmodel
@{
    viewdata["title"] = "list";
}

<h1>list</h1>

<p>
    <a class="btn btn-primary" asp-page="add">add</a>
</p>
<table class="table">
    <tr>
        <th>id</th>
        <th>name</th>
        <th>age</th>
        <th>class</th>
        <th>sex</th>
        <th></th>
    </tr>
    @foreach (var student in model.students)
    {
        <tr>
            <td>@student.id</td>
            <td>@student.name</td>
            <td>@student.age</td>
            <td>@student.class</td>
            <td>@student.sex</td>
            <td>
                <a class="btn btn-primary" asp-page="update" asp-route-id="@student.id">update</a>
                <a class="btn btn-danger" href="/student/delete?id=@student.id" >delete</a>
            </td>
        </tr>
    }

</table>

listmodel类混合了mvc的controller跟model的概念。它本身可以认为是mvc里面的那个model,它包含的数据可以被razor试图引擎使用,用来生成html,比如它的students属性;但是它又包含方法,可以用来处理业务逻辑,这个方法可以认为是controller中的action。方法通过特殊的前缀来跟前端的请求做绑定,比如onget方法就是对get请求作出响应,onpost则是对post请求作出响应。
运行一下并且访问/student/list:

列表页面可以正常运行了。

使用asp-page进行页面间导航

列表页面上有几个按钮,比如新增、删除等,点击的时候希望跳转至不同的页面,可以使用asp-page属性来实现。asp-page属性不是html自带的属性,显然这是razor pages为我们提供的。

<p>
    <a class="btn btn-primary" asp-page="add">add</a>
</p>

上面的代码在a元素上添加了asp-page=”add”,表示点击这个a连接会跳转至同级目录的add页面。html页面之间的导航不管框架怎么封装无非就是url之间的跳转。显然这里asp-page最后会翻译成一个url,看看生成的页面源码:

<a class="btn btn-primary" href="/student/add">add</a>

跟我们想的一样,最后asp-page被翻译成了href=”/student/add”。

使用asp-route-xxx进行传参

页面间光导航还不够,更多的时候我们还需要进行页面间的传参。比如我们的更新按钮,需要跳转至update页面并且传递一个id过去。

<a class="btn btn-primary" asp-page="update" asp-route-id="@student.id">update</a>

我们使用asp-route-id来进行传参。像这里的a元素进行传参,无非是放到url的querystring上。让我们看一下生成的html源码:

<a class="btn btn-primary" href="/student/update?id=2">update</a>

不出所料最后id作为querystring被组装到了url上。
上面演示了razor pages的导航跟传参,使用了几个框架内置的属性,但其实我们根本可以不用这些东西就可以完成,使用标准的html方式来完成,比如删除按钮:

<a class="btn btn-danger" href="/student/delete?id=@student.id" >delete</a>

上面的写法完全可以工作,并且更加清晰明了,谁看了都知道是啥意思。
小小的吐槽下微软:像asp-page这种封装我是不太喜欢的,因为它掩盖了html、http工作的本质原理。这样会造成很多同学知道使用asp-page怎么写,但是换个框架就不知道怎么搞了。我见过号称精通asp.net的同学,但是对html、特别是对http一无所知。当你了解了真相后,甭管你用什么技术,看起来其实都是一样的,都是套路。

实现新增(student/add)页面

新增页面提供几个输入框输入学生信息,并且可以提交到后台。
修改addmodel类:

   public class addmodel : pagemodel
   {
       private readonly istudentrepository _studentrepository;
       public addmodel(istudentrepository studentrepository)
       {
           _studentrepository = studentrepository;
       }
       public void onget()
       {
       }

       [bindproperty]
       public student student { get; set; }

       public iactionresult onpostsave()
       {
           _studentrepository.add(student);
           return redirecttopage("list");
       }
   }

修改add.cshtml页面

@page
@model razorpagecrud.addmodel
@{
    viewdata["title"] = "add";
}

<h1>add</h1>

<form method="post">
    <div class="form-group">
        <label>id</label>
        <input type="number" asp-for="student.id" class="form-control" />
    </div>
    <div class="form-group">
        <label>name</label>
        <input type="text" asp-for="student.name" class="form-control" />
    </div>
    <div class="form-group">
        <label>age</label>
        <input type="number" asp-for="student.age" class="form-control" />
    </div>
    <div class="form-group">
        <label>class</label>
        <input type="text" asp-for="student.class" class="form-control" />
    </div>
    <div class="form-group">
        <label>sex</label>
        <input type="text" asp-for="student.sex" class="form-control" />
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary" asp-page-handler="save">save</button>
        <a asp-page="list" class="btn btn-dark">cancel</a>
    </div>
</form>

add页面使用一个form表单作为容器,里面的文本框使用asp-for跟model的student属性建立联系。
运行一下:

asp-for会把关联的属性字段的值作为input元素的value的值,会把关联的属性名+字段的名称作为input元素的name属性的值。看看生成的html源码:

<input type="text" class="form-control" id="student_name" name="student.name" value="">

使用asp-page-handler来映射模型方法

我们的save是一次post提交,显然我们需要一个后台方法来接受这次请求并处理它。使用asp-page-handler=”save”可以跟模型的onpostsave方法做映射。onpost前缀表示对post请求做响应,这又有点像webapi。那么asp-page-handler为什么能映射模型的方法呢?继续看看生成的源码:

<button type="submit" class="btn btn-primary" formaction="/student/add?handler=save">save</button>

看到这里就明白了。最后生成的button上有个formaction属性,值为/student/add?handler=save。formaction相当于在form元素上指定action属性的提交地址,并且在url上附带了一个参数handler=save,这样后台就能查找具体要执行哪个方法了。不过据我的经验formaction属性存在浏览器兼容问题。

使用bindpropertyattribute进行参数绑定

光能映射后台方法还不够,我们还需要把前端的数据提交到后台,并且拿到它。这里可以使用bindpropertyattribute来自动完成提交的表单数据跟模型属性之间的映射。这样我们的方法可以是无参的方法。

        [bindproperty]
        public student student { get; set; }

看到这里突然有种mvvm模式的既视感了。虽然不是实时的双向绑定,但是也实现了简单的前后端绑定技术。另外提一句既然我们前端的数据是通过表单提交,那么跟mvc一样,使用fromformattribute其实一样可以进行参数绑定的。

public iactionresult onpostsave([fromform] stuend student)

这有获取表单数据毫无问题。

在后台方法进行页面导航

当保存成功后需要使页面跳转到列表页面,可以使用redirecttopage等方法进行跳转,onpostsave方法的返回值类型也改成iactionresult,这就非常mvc了,跟action方法一模一样的套路。

public iactionresult onpostsave()
        {
            _studentrepository.add(student);
            return redirecttopage("list");
        }

修改编辑(student/update)页面

修改,删除页面就没什么好多讲的了,使用前面的知识点轻松就能实现。
修改cshtml模板:

@page
@model razorpagecrud.updatemodel
@{
    viewdata["title"] = "update";
}

<h1>update</h1>

<form method="post">
    <div class="form-group">
        <label>id</label>
        <input type="number" asp-for="student.id" class="form-control" />
    </div>
    <div class="form-group">
        <label>name</label>
        <input type="text" asp-for="student.name" class="form-control" />
    </div>
    <div class="form-group">
        <label>age</label>
        <input type="number" asp-for="student.age" class="form-control" />
    </div>
    <div class="form-group">
        <label>class</label>
        <input type="text" asp-for="student.class" class="form-control" />
    </div>
    <div class="form-group">
        <label>sex</label>
        <input type="text" asp-for="student.sex" class="form-control" />
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary" asp-page-handler="edit">save</button>
        <a asp-page="list" class="btn btn-dark">cancel</a>
    </div>
</form>

修改updatemodel类:

    public class updatemodel : pagemodel
    {
        private readonly istudentrepository _studentrepository;
        public updatemodel(istudentrepository studentrepository)
        {
            _studentrepository = studentrepository;
        }
        public void onget(int id)
        {
            student = _studentrepository.get(id);
        }

        [bindproperty]
        public student student { get; set; }

        public iactionresult onpostedit()
        {
            _studentrepository.update(student);

            return redirecttopage("list");
        }
    }

运行一下:

修改删除(student/delete)页面

删除页面跟前面一样没什么好多讲的了,使用前面的知识点轻松就能实现。
修改delete.cshtml模板:

@page
@model razorpagecrud.deletemodel
@{
    viewdata["title"] = "delete";
}

<h1>delete</h1>
<h2 class="text-danger">
    确定删除?
</h2>
<form method="post">
    <div class="form-group">
        id: @model.student.id
    </div>
    <div class="form-group">
        name:@model.student.name
    </div>
    <div class="form-group">
        age: @model.student.age
    </div>
    <div class="form-group">
        class: @model.student.class
    </div>
    <div class="form-group">
        sex: @model.student.sex
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary" asp-page-handler="delete" asp-route-id="@model.student.id">delete</button>
        <a asp-page="list" class="btn btn-dark">cancel</a>
    </div>
</form>

修改deletemodel类:

     public class deletemodel : pagemodel
    {
        private readonly istudentrepository _studentrepository;
        public deletemodel(istudentrepository studentrepository)
        {
            _studentrepository = studentrepository;
        }

        public void onget(int id)
        {
            student = _studentrepository.get(id);
        }

        public student student { get; set; }

        public iactionresult onpostdelete(int id)
        {
            _studentrepository.delete(id);

            return redirecttopage("list");
        }
    }  

运行一下:

总结

通过上的简单示例,对razor pages有了大概的了解。razor pages本质上对mvc模式的简化,后台模型聚合了controller跟model的的概念。并且提供了一些内置html属性实现绑定技术。有人说razor pages是webform的继任者,我倒不觉得。个人觉得它更像是mvc/mvvm的一种混合。[bindproperty]有点像wpf里的依赖属性,onpostxxx方法就像是command命令;又或者[bindproperty]像vue的data属性上的字段,onpostxxx像methods里的方法;又或者整个model像极了angularjs的$scope,混合了数据跟方法。只是razor pages毕竟是服务端渲染,不能进行实时双向绑定而已。最后,说实话通过简单的体验,razor pages开发模式跟mvc模式相比并未有什么特殊的优点,不知道后续发展会如何。