C#中的MVC

M(模型model):实体类用来做数据传递的
V(试图view):Razor动态页面.cshtml文件,html用来展示给用户看的
C(控制器controller):后台的逻辑处理代码(处理请求,数据处理,生成响应)对试图的逻辑处理

同时了解一下NuGet程序包identity用户权限包,和EFcore功能一样并且多了用户权限的功能。
用户权限核心包:Microsoft.AspNetCore.Identity.EntityFrameworkCore 上下文类放父类继承改为 : IdentityDbContext
错误信息提示包:Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
UI界面生成包:Microsoft.AspNetCore.Identity.UI 这个包有自动生成注册信息之类的UI界面功能

顶级程序Program类的了解

 主要是有两部分组成:1.添加服务,2.配置管道

using Microsoft.AspNetCore.Identity;//权限管理
using Microsoft.EntityFrameworkCore;//EFcore框架
using MVC.Data;//数据库上下文类

var builder = WebApplication.CreateBuilder(args);//1、定义主机创建者对象
//2、把服务添加到对象里:ioc容器有管理功能,自动(创建和释放),使用到某个类都可以在这里添加。
//添加服务:把服务添加到IOC容器。就是把类添加到容器,我们在外调用就不需要每次去实例化了,直接可以用。asp.net core底层也是构建在IOC容器里的。
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");//Configuration配置对象内获取到服务器的很多信息
builder.Services.AddDbContext<ApplicationDbContext>(options => //把数据库上下文类添加进来,这个程序都可以使用数据库了,不需要在实例化对象。
    options.UseSqlServer(connectionString));//参数是配置文件appsettings.json数据库连接字符串,options选择数据库.UseSqlServer指SQL数据库。
builder.Services.AddDatabaseDeveloperPageExceptionFilter();//异常页的服务,如:生成数据库时要不要迁移的提示页面就是他生成的(是个过滤器)。
//Identity身份认证框架相关的实体类表UI界面那的表。
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)//参数是委托,权限通过账号登录
    .AddEntityFrameworkStores<ApplicationDbContext>();//Identity框架自动实现了增删改查的方法,把上下文传递进来即可,不需要手动写增删改查。
builder.Services.AddControllersWithViews();//MVC服务组,和MVC框架相关的都可以使用了

var app = builder.Build();//3、创建主机

//4、配置请求管道:(通过中间件监管起来)代码顺序不能乱
if (app.Environment.IsDevelopment())//获取环境变量是开放模式Development还是部署模式Production,在启动设置文件launchSettings.json里获取信息
{
    app.UseMigrationsEndPoint();//处理数据库自动迁移的请求,不需要手动命令迁移,在用户注册权限哪里用到一次
}
else
{
    app.UseExceptionHandler("/Home/Error");//错误页,有错误自动跳转
    
    app.UseHsts();//协议中间件,设置安全访问
}

app.UseHttpsRedirection();//遇到http也会自动跳转为https.
app.UseStaticFiles();//允许访问静态文件的中间件.如:网页html文件。必须在wwwroot文件夹下,这个代码必须在协议访问后面才安全

app.UseRouting();//选择路由,判断页面是MVC还是Razor动态页来决定走什么路由

app.UseAuthentication();//身份认证
app.UseAuthorization();//授权

app.MapControllerRoute( //mvc路由
    name: "default", //路由名称:默认路由
    pattern: "{controller=Home}/{action=Index}/{id?}");//默认路径:home/index,id是参数
app.MapRazorPages();//允许访问razor路由

app.Run();//5、运行

mvc路由其实是控制器Controllers里的某个方法

public IActionResult Index(int? id)//返回一个页面,参数是接收路由传递
{
    return View();//无参返回和方法同名的页面路由,参数可以给路径跳转其他页面
}
public IActionResult Privacy()//对应个方法对应一个Views文件夹页面名
{
    return View();//如果方法名没有页面,可以用view参数路径指定一个有效页面
}

控制器定义的时候HomeController类,而Home对应的是Views试图的一个文件夹,路由便通过这个Home文件夹去找页面。

控制器Controller

基类:ControllerBase不支持试图,后面c#封装了Controller专门操作试图的类。
三个重要对象:
htppContext:请求和需要的上下文对象,包含链接的信息,包括一些中间件,以及身份验证和授权User对象等等。
Reqhest:Http请求,请求头,查询字符串,内容类型等等。
Response:返回网页响应的信息。
控制器与页面的数据传递(5种)
后台

public IActionResult Privacy()//返回值是一个处理试图类型
{
    ViewBag.User1 = "张三";//是对ViewData的包装改写,一样的效果,注意User1是取名,没有提示
    ViewData["User2"] = "李四";//键值对的方式保存,只能保存于当前页
    TempData["User3"] = "王五";//夸请求保存数据,夸网页,上面周期长一点
    HttpContext.Session.SetString("User4", "赵六");//http上下文类的session存值
    object User5 = "田七";//model模型,可以是字符串,也可以是实体类对象,都是一样的取值
    return View(User5);//传模型必须是obj类型,f12转到定义看看view类型
}

前台

@{
    ViewData["Title"] = "标题:相当于后台代码";
}
<h1>@ViewData["Title"]</h1>

<h1>@ViewBag.User1</h1>
<h1>@ViewData["User2"]</h1>
<h1>@TempData["User3"]</h1>
<h1>@Context.Session.GetString("User4")</h1>
@model string
<h1>@Model</h1>

使用session需要【Program.cs】顶级程序,添加服务和启动服务

builder.Services.AddSession();//添加session
app.UseSession();//启动session

 多数据传递用model,传对象

Student stu = new Student() { id = 1, name = "李四" };//实体类
            return View(stu);//多数据传递,复杂数据,两个参数,传第二个时是路由

model前台调用注意,导入数据用小model,调用数据显示用大Model

@model MVC.Controllers.Student //引用实体类命名空间
@Model.name @*显示实体类的数据,大Model是自动绑定实体类数据,相当于实例化后的对象*@

控制器的构造函数作用是拿到顶级程序Program类里添加的所有服务。

 log是日志服务,c#默认自带的,所以不需要调用,其他服务可以通过参数传递进来,在通过变量接收即可。

log插件:日志文件(两种:Log4Net和Nlog)

使用数据库后台需要NuGet引入:System.Data.SqlClient 

if exists(select * from sys.databases where name='LogManager')--也可以用dbid=db_ID('LogManager')一样的只是不好记,习惯用name='NetBarDB'
    begin  --开始相当于{
        use master                    --使用 系统master数据库
        alter database LogManager            --更改alter 数据库database 数据库名称NetBarDB
        set single_user                --设置数据库为SINGLE_USER【单个用户】模式,减少锁定时间 
        --一般很快, 但有时数据库可能有人在链接,就会很慢;可以加上一个with rollback immediate, 将未提交的修改立刻回滚 
        with rollback immediate        --立即断开所有连接;with【跟,和...一起】rollback 【回滚,恢复到初始状态】immediate【立刻,马上】
        drop database LogManager  --删除数据库drop database 数据库名,为了避免数据库还在使用状态无法删除,所以要加上上面的代码
    end       --结束相当于}        --drop【使降落,减少】语句将表所占用的空间全部释放,达到删除空间的效果

create database LogManager    --创建数据库create database 数据库名
go    --批处理(数据库无法自动运行下一句代码,需要加go来继续代码的运行)
use LogManager                --打开数据库
go
/* 
  创建Log4net的表
*/

create table [Log4Net](
    [Id] int primary key identity(1,1) not null,
    [Date] datetime not null,
    [Thread] varchar(255) not null,
    [Level] varchar(50) not null,
    [Logger] varchar(255) not null,
    [Message] varchar(4000) not null,
    [Exception] varchar(2000) null
) 
go
/* 
  创建Log4net的表
*/
create table [NLog](
    [Id] int primary key identity(1,1) not null,
    [Application] nvarchar(50) NOT NULL,
    [Logged] datetime NOT NULL,
    [Level] nvarchar(50) NOT NULL,
    [Message] nvarchar(max) NOT NULL,
    [Logger] nvarchar(250) NULL,
    [Callsite] nvarchar(max) NULL,
    [Exception] nvarchar(max) NULL
)
go
select * from Log4Net
select * from NLog

Log4Net

顶级程序【Program.cs】添加调用配置文件的代码

builder.Logging.AddLog4Net("CfgFile/log4net.Config");//添加Log4Net日志的配置文件,需要Nuget引入:Microsoft.Extensions.Logging.Log4Net.AspNetCore

配置文件的代码如下,需要在【CfgFile】目录下创建【log4net.Config】配置文件

<?xml version="1.0" encoding="utf-8"?>
<log4net>
    <!-- Define some output appenders -->
    <appender name="rollingAppender" type="log4net.Appender.RollingFileAppender">
        <file value="log4\log.txt" />
        <!--追加日志内容-->
        <appendToFile value="true" />

        <!--防止多线程时不能写Log,官方说线程非安全-->
        <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />

        <!--可以为:Once|Size|Date|Composite-->
        <!--Composite为Size和Date的组合-->
        <rollingStyle value="Composite" />

        <!--当备份文件时,为文件名加的后缀-->
        <datePattern value="yyyyMMdd.TXT" />

        <!--日志最大个数,都是最新的-->
        <!--rollingStyle节点为Size时,只能有value个日志-->
        <!--rollingStyle节点为Composite时,每天有value个日志-->
        <maxSizeRollBackups value="20" />

        <!--可用的单位:KB|MB|GB-->
        <maximumFileSize value="3MB" />

        <!--置为true,当前最新日志文件名永远为file节中的名字-->
        <staticLogFileName value="true" />

        <!--输出级别在INFO和ERROR之间的日志-->
        <filter type="log4net.Filter.LevelRangeFilter">
            <param name="LevelMin" value="ALL" />
            <param name="LevelMax" value="FATAL" />
        </filter>
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%date [%thread] %-5level %logger - %message%newline"/>
        </layout>
    </appender>

    <!--SqlServer形式-->
    <!--log4net日志配置:http://logging.apache.org/log4net/release/config-examples.html -->
    <appender name="AdoNetAppender_SqlServer" type="log4net.Appender.AdoNetAppender">
        <!--日志缓存写入条数 设置为0时只要有一条就立刻写到数据库-->
        <bufferSize value="0" />
        <connectionType value="System.Data.SqlClient.SqlConnection,System.Data.SqlClient, Version=4.6.1.3, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
        <connectionString value="Data Source=.;Initial Catalog=LogManager;Persist Security Info=True;User ID=sa;Password=123456" />
        <commandText value="INSERT INTO Log4Net ([Date],[Thread],[Level],[Logger],[Message],[Exception]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception)" />
        <parameter>
            <parameterName value="@log_date" />
            <dbType value="DateTime" />
            <layout type="log4net.Layout.RawTimeStampLayout" />
        </parameter>
        <parameter>
            <parameterName value="@thread" />
            <dbType value="String" />
            <size value="255" />
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%thread" />
            </layout>
        </parameter>
        <parameter>
            <parameterName value="@log_level" />
            <dbType value="String" />
            <size value="50" />
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%level" />
            </layout>
        </parameter>
        <parameter>
            <parameterName value="@logger" />
            <dbType value="String" />
            <size value="255" />
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%logger" />
            </layout>
        </parameter>
        <parameter>
            <parameterName value="@message" />
            <dbType value="String" />
            <size value="4000" />
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%message" />
            </layout>
        </parameter>
        <parameter>
            <parameterName value="@exception" />
            <dbType value="String" />
            <size value="2000" />
            <layout type="log4net.Layout.ExceptionLayout" />
        </parameter>
    </appender>

    <root>

        <!--控制级别,由低到高: ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF-->
        <!--OFF:0-->
        <!--FATAL:FATAL-->
        <!--ERROR: ERROR,FATAL-->
        <!--WARN: WARN,ERROR,FATAL-->
        <!--INFO: INFO,WARN,ERROR,FATAL-->
        <!--DEBUG: INFO,WARN,ERROR,FATAL-->
        <!--ALL: DEBUG,INFO,WARN,ERROR,FATAL-->
        <priority value="ALL"/>

        <level value="INFO"/>
        <appender-ref ref="rollingAppender" />
        <appender-ref ref="AdoNetAppender_SqlServer" />
    </root>
</log4net>

Nuget

顶级程序【Program.cs】添加调用配置文件的代码

builder.Logging.AddNLog("CfgFile/NLog.config");//需要引入:NLog.Web.AspNetCore,如果连接数据库还需要安装:NLog.Database

配置文件的代码如下,需要在【CfgFile】目录下创建【NLog.Config】配置文件

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
      autoReload="true"
      throwExceptions="false"
      internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">

    <!-- optional, add some variables
  https://github.com/nlog/NLog/wiki/Configuration-file#variables
  -->
    <variable name="myvar" value="myvalue"/>

    <!--
  See https://github.com/nlog/nlog/wiki/Configuration-file
  for information on customizing logging rules and outputs.
   -->
    <targets>
        <!--
    add your targets here
    See https://github.com/nlog/NLog/wiki/Targets for possible targets.
    See https://github.com/nlog/NLog/wiki/Layout-Renderers for the possible layout renderers.
    -->
        <target name="AllDatabase" xsi:type="Database"
              dbProvider="System.Data.SqlClient.SqlConnection, System.Data.SqlClient"
              connectionString="Data Source=.;Initial Catalog=LogManager;Persist Security Info=True;User ID=sa;Password=123456"
              commandText="insert into dbo.NLog (Application, Logged, Level, Message,Logger, CallSite, Exception) values (@Application, @Logged, @Level, @Message,@Logger, @Callsite, @Exception);">
            <parameter name="@application" layout="AspNetCoreNlog" />
            <parameter name="@logged" layout="${date}" />
            <parameter name="@level" layout="${level}" />
            <parameter name="@message" layout="${message}" />
            <parameter name="@logger" layout="${logger}" />
            <parameter name="@callSite" layout="${callsite:filename=true}" />
            <parameter name="@exception" layout="${exception:tostring}" />
        </target>

        <target xsi:type="File" name="allfile" fileName="NLog\nlog-all-${shortdate}.log"
                layout="${longdate}|${logger}|${uppercase:${level}}|${message} ${exception}" />
        <!--同样是将文件写入日志中,写入的内容有所差别,差别在layout属性中体现。写入日志的数量有差别,差别在路由逻辑中体现-->
        <target xsi:type="File" name="ownFile-web" fileName="NLog\nlog-my-${shortdate}.log"
                 layout="${longdate}|${logger}|${uppercase:${level}}|${message} ${exception}" />
        <target xsi:type="Null" name="blackhole" />
        <!--
    Write events to a file with the date in the filename.
    <target xsi:type="File" name="f" fileName="${basedir}/logs/${shortdate}.log"
            layout="${longdate} ${uppercase:${level}} ${message}" />
    -->
    </targets>

    <rules>
        <logger name="*" minlevel="Trace" writeTo="AllDatabase" />
        <!-- add your logging rules here -->
        <!--路由顺序会对日志打印产生影响。路由匹配逻辑为顺序匹配。-->
        <!--All logs, including from Microsoft-->
        <logger name="*" minlevel="Trace" writeTo="allfile" />
        <!--Skip Microsoft logs and so log only own logs-->
        <!--以Microsoft打头的日志将进入此路由,由于此路由没有writeTo属性,所有会被忽略-->
        <!--且此路由设置了final,所以当此路由被匹配到时。不会再匹配此路由下面的路由。未匹配到此路由时才会继续匹配下一个路由-->
        <logger name="Microsoft.*" minlevel="Trace"  final="true" />
        <!--上方已经过滤了所有Microsoft.*的日志,所以此处的日志只会打印除Microsoft.*外的日志-->
        <logger name="*" minlevel="Trace" writeTo="ownFile-web" />
        <!--
    Write all events with minimal level of Debug (So Debug, Info, Warn, Error and Fatal, but not Trace)  to "f"
    <logger name="*" minlevel="Debug" writeTo="f" />
    -->
    </rules>
</nlog>

调用演示:两种都是一样的调用方式

public class SecondController : Controller
{
    private readonly ILogger<SecondController> _Logger;//负责记录日志信息,是log的一个实例
    private readonly ILoggerFactory _LoggerFactory;//也可以注入一个:long工厂,集合
    public SecondController(ILogger<SecondController> logger, ILoggerFactory loggerFactory)
    {//构造函数注入
        this._Logger = logger;
        this._Logger.LogInformation($"{this.GetType().Name} 被构造了。。。_Logger");

        this._LoggerFactory = loggerFactory;
        ILogger<SecondController> _Logger2 = this._LoggerFactory.CreateLogger<SecondController>();
        _Logger2.LogInformation($"{this.GetType().Name} 被构造了。。。_Logger2");
    }
    public IActionResult Index()
    {
        ILogger<SecondController> _Logger3 = this._LoggerFactory.CreateLogger<SecondController>();
        _Logger3.LogInformation($"Index 被执行了。。。。。_Logger3");
        this._Logger.LogInformation($"Index 被执行了。。。");
        return View();
    }

    public IActionResult Level()
    {
        _Logger.LogDebug("调试");
        _Logger.LogInformation("一把日志信息");
        _Logger.LogWarning("警告");
        _Logger.LogError("错误");
        _Logger.LogTrace("跟踪");
        _Logger.LogCritical("严重崩溃");

        return new JsonResult(new { Success = true });
    }
}

过滤器 Filter:其实就是特性

作用:给多个控制器添加一下新功能,分为:系统过滤器和自定义过滤器
标注:3种方式
1.在控制器类上:作用于整个当前控制器的方法。
2.在方法上:作用于某个控制器的一个方法。
3.通过服务的形式配置:作用于全局。就是在顶级程序里添加服务。

builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(typeof(MyCustomFilter));//添加自定义过滤器,作用于全局
});

整个过滤器是自定义的需要写代码。这里只是演示,怎么在全局里添加过滤器。
定义于方法上的过滤器:这里用的系统权限过滤器,需要引入using Microsoft.AspNetCore.Authorization;过滤器命名空间

[Authorize(Roles = "Admin")] //系统过滤器,说明管理员才能访问这个页面。
public IActionResult Index()
{
    return View();
}

 演示一个身份登录验证的功能,首先添加服务:启用角色管理

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)//参数是委托,权限通过账号登录
    .AddRoles<IdentityRole>()//启用角色管理的,实现权限访问
    .AddEntityFrameworkStores<ApplicationDbContext>();//Identity框架自动实现了增删改查的方法,把上下文传递进来即可,不需要手动写增删改查。 

创建一个权限控制器 RolesController

using Microsoft.AspNetCore.Identity;//角色权限管理
using Microsoft.AspNetCore.Mvc;
using MVC.Data;//数据库上下文类

namespace MVC.Controllers
{
    public class RolesController : Controller
    {
        private readonly ApplicationDbContext db;//获取数据库上下文类
        private readonly UserManager<IdentityUser> userManager;//可以通过鼠标右键参数快速创建字段来生成代码。也可以手动写
        private readonly RoleManager<IdentityRole> roleManager;//获取权限

        public RolesController(ApplicationDbContext db, UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager)
        {
            this.db = db;
            this.userManager = userManager;//获取用户
            this.roleManager = roleManager;//这里只是简单的演示,通过角色来控制用户的行为。
        }

        private string AdminRole = "Admin";//随便创建两个字段做验证用
        private string UserEmail = "Admin@163.com";
        public async Task<IActionResult> Index()//异步编程:有await返回值必须是Task代替void,这里有数据类型加Task<返回值类型>,返回页面。
        {
            await roleManager.CreateAsync(new IdentityRole(AdminRole));//创建角色

            IdentityUser user = new IdentityUser() { UserName = UserEmail, Email = UserEmail, EmailConfirmed = true };
            await userManager.CreateAsync(user, UserEmail);//创建管理用户
            await userManager.AddToRoleAsync(user, AdminRole);//给用户添加一个角色

            return Redirect("/");//跳转到首页,Home下的index
        }
    }
}

异步编程:提高服务器的吞吐量(I/o)就是高效的使用cup工作,分为并发和并行。
历史演变:Thread线程---》ThreadPool线程池---》Task任务(async和await)两个关键字。
并发:指一个cpu多线程交替式运行,比如说,同时下载3个文件,一个下载点,最后差不多一起完成的任务。如果没有并发则是一个完成后才能执行第二个任务。
并行:指多个CPU来同时运多个线程。
缓存ResponseCache过滤器:为了提高响应时间和可伸缩性,缓存由动作方法生成的HTTP响应 ,提高下次访问页面的速度。比如缓存一小时,一天等等。有时候太长太短也不好。根据情况定。

//分级缓存的,Duration表示缓存时长,Location为None测前面一个参数无效,表示不缓存, NoStore为true表示不缓存前面两个参数关闭,将不起作用。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
    return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

Location有3个值:

 自定义路由Route过滤器:不需要通过控制器访问,直接访问过滤器名即可跳转。

[Route("eee")]//自定义路由:网页直接/eee访问就跳转到指定的方法页面下了
public IActionResult Privacy()
{
    return View();
}

 项目案例

创建一个商品实体类,做数据库迁移

using System.ComponentModel.DataAnnotations;//特性
using System.ComponentModel.DataAnnotations.Schema;//特性NotMapped表不映射

namespace MVC.Models
{
    public class Product
    {
        public int Id { get; set; }//编号
        public string Name { get; set; }//商品名称
        public decimal Price { get; set; }//单价
        public short Stock { get; set; }//库存
        public string? ImagUrl { get; set; }//图片路径,可为空
        [NotMapped]//不映射到数据库
        [Display(Name = "商品图片")]
        public IFormFile? formFile { get; set; }//图片/文件的上传下载,IFormFile类型能存放其他文件。
    }
}

在上下文类中设置数据库创建属性

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using MVC.Models;

namespace MVC.Data
{
    public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<Product> Product { get; set; } //添加实体类数据库中,使用命令迁移
    }
}

通过命令迁移生成数据库:1.add-migration init-Mig 生成迁移代码   2.update-database 执行到数据库

 首先在布局页加个a标签做跳转到商品首页,避免每次去地址栏输入

<a class="nav-link text-dark" asp-area="" asp-controller="Products" asp-action="Index">跳转商品首页</a>

在同级控制器下asp-controller属性可以不用加。asp-route-id="@item.Id"传递值给下个页面。用来做删除和修改。

然后在通过后台代码在首页显示数据

public class ProductsController : Controller
{
    private readonly ApplicationDbContext _context;

    public ProductsController(ApplicationDbContext context)
    {
        _context = context; //获取数据库上下文类
    }

    // GET:显示所有数据
    public async Task<IActionResult> Index()
    {
        return View(await _context.Product.ToListAsync()); //把表转成list显示出来
    }
}

获取属性名,也就是每一列的表头名称;这种方法慢慢的被标签属性所代替。(这里的model张三表达式的一个变量,有参有返回值的lambda表达式)

@Html.DisplayNameFor(model => model.Name)

model是引入,这里是引入实体类这个类型。

@model IEnumerable<MVC.Models.Product>

Model,相当于实例化对象后的数据。所以我们循环遍历的时候是用大小Model来遍历的。

@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Stock)
            </td>
            <td>
                <img src="@("/images/"+item.ImagUrl)" alt="没有图片" width="100px" />
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>

添加商品

[HttpPost]//提交数据都用post请求
[ValidateAntiForgeryToken] //防止伪造攻击,post提交数据都带上这个特性。
public async Task<IActionResult> Create([Bind("Id,Name,Price,Stock,ImagUrl")] Product product)//特性Bind防止重复提交数据,页面Model会把数据传给参数
{
    if (ModelState.IsValid)//后台拿到页面的Model数据进行验证js或特性验证。
    {
        _context.Add(product);//添加到上下文中
        await _context.SaveChangesAsync();//异步保存到数据库,异步就是多线程处理。
        return RedirectToAction(nameof(Index));//跳转首页,nameof叫同步变化,比如把类名index变为111,nameof的参数也会自动变为111,避免写死的字符串。
    }
    return View(product);//数据又返回到页面Model,所以输入框里的数据依然没变
}

 图片上传,在这里遇到一个文件上传的错误,注意关闭热重载。

文件上传的标签,必须有name属性对应实体类属性名,后台才能获取到上传的文件信息,标签是通过name属性传给后台的。

<input asp-for="formFile" class="form-control" name=formFile />

提交文件数据时form标签必须添加enctype="multipart/form-data"的属性:文件的编码格式,有它才能提交带文件的表单数据。

<form asp-action="Create" enctype="multipart/form-data">

保存图片的后台代码,创建一个私有方法来写创建和修改的相同代码

private async Task<string> SaveImage(IFormFile formFile)//参数是上传文件,返回文件名。给创建和修改的方法一起使用。
{
    string fileName = Guid.NewGuid().ToString("N") + ".jpg";//自定义文件名,不重复,以时间定义
    string filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/images", fileName);//拼接保存路径Combine

    using (var stream = System.IO.File.Create(filePath))//创建一个图片文件流,用来保存图片
    {
        await formFile.CopyToAsync(stream);//把图片拷贝到文件流上面
    }
    return fileName;//返回文件名。
}

创建数据到数据库的后台代码

[HttpPost]//提交数据都用post请求
[ValidateAntiForgeryToken] //防止伪造攻击,提交数据都带上这个特性。
public async Task<IActionResult> Create([Bind("Id,Name,Price,Stock,ImagUrl,formFile")] Product product)//特性Bind防止重复提交数据
{
    if (ModelState.IsValid)//后台拿到页面的Model数据进行验证js或特性验证。
    {
        if (product.formFile != null)//如果上传图片不为空
        {
            product.ImagUrl = await SaveImage(product.formFile);//保存文件名到数据库,异步调用
        }
        _context.Add(product);//添加到上下文中
        await _context.SaveChangesAsync();//异步保存到数据库,异步就是多线程处理。
        return RedirectToAction(nameof(Index));//跳转首页,nameof是动态的,比如把类名index变为111,nameof的参数也会自动变为111
    }
    return View(product);//数据又返回到页面,所以输入框里的数据依然没变
}

在前台显示图片

<img src="@("/images/"+item.ImagUrl)" alt="没有图片" width="100px" />

 修改数据

是通过id先查到需要修改的数据信息,如果页面没有id可以用,asp-route-id="@Model?.Id",意思是实体类如果有数据就查id,如果没有数据就返回null。

<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |

后台通过get请求根据id查询到数据。

public async Task<IActionResult> Edit(int? id)
{
    if (id == null || _context.Product == null)//如果id没数据或者实体类没数据
    {
        return NotFound();//返回状态码,就是没找到页面,因为提交数据成功不成功是用状态码来表示的。
    }

    var product = await _context.Product.FindAsync(id);//工具id去数据库查询数据。
    if (product == null)//没查询到数据。
    {
        return NotFound();//没找到页面
    }
    return View(product); //查询到数据,就显示出来
}

 创建一个删除图片的公共方法

private void RemoveImage(string? fileName)
{
    if (fileName != null)
    {
        string filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/images", fileName);//获取文件路径
        System.IO.File.Delete(filePath);//删除图片
    }
}

post提交修改数据

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Name,Price,Stock,ImagUrl,formFile")] Product product)
{
    if (id != product.Id) //id是页面直接传过来的id,和我们实体类的Id是否一样
    {
        return NotFound();//找不到页面。
    }

    if (ModelState.IsValid)//特性验证模型填写的值是否通过
    {
        try
        {
            if (product.formFile != null)//如果上传图片不为空
            {
                if (product.ImagUrl != null)//如果图片存在
                {
                    RemoveImage(product.ImagUrl);//删除老图片
                }
                product.ImagUrl = await SaveImage(product.formFile);//保存文件名到数据库,异步调用
            }
            _context.Update(product);//提交修改
            await _context.SaveChangesAsync(); //保存到数据库
        }
        catch (DbUpdateConcurrencyException)//修改异常
        {
            if (!ProductExists(product.Id))//下面的方法调用查询id是否存在,如果id不存在。
            {
                return NotFound();//没有找到
            }
            else
            {
                throw;//抛出异常
            }
        }
        return RedirectToAction(nameof(Index));//修改成功后就返回首页
    }
    return View(product);//为修改成功返回模型继续修改。
}
private bool ProductExists(int id)
{
    return _context.Product.Any(e => e.Id == id);//Any包含的意思,查询id是否存在
}

删除已存在文件,注意,添加一个隐藏标签属性保存一下旧文件路径,得到旧文件路径才能删除文件。

<input type="hidden" asp-for="ImagUrl" />

删除商品

将id传给后台,查询删除的数据信息

<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>

get请求显示需要删除的数据

public async Task<IActionResult> Delete(int? id)
{
    if (id == null || _context.Product == null)
    {
        return NotFound();
    }

    var product = await _context.Product
        .FirstOrDefaultAsync(m => m.Id == id);//查询到第一条数据
    if (product == null)
    {
        return NotFound();
    }

    return View(product);//找到了数据,返回模型
}

然后通过隐藏标签将id传给后台,删除

<form asp-action="Delete">
        <input type="hidden" asp-for="Id" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-action="Index">Back to List</a>
</form>

post提交删除的数据

[HttpPost, ActionName("Delete")]//表示提交的方法为Delete,在此方法运行。
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    if (_context.Product == null)//如果模型没有数据
    {
        return Problem("Entity set 'ApplicationDbContext.Product'  is null.");
    }
    var product = await _context.Product.FindAsync(id);
    if (product != null)
    {
        _context.Product.Remove(product);//删除数据
        if (product.ImagUrl != null)
        {
            RemoveImage(product.ImagUrl);//删除图片
        }
    }

    await _context.SaveChangesAsync();//保存数据库
    return RedirectToAction(nameof(Index));//返回首页
}

 

posted @ 2022-11-17 01:35  Akai_啊凯  阅读(1492)  评论(0编辑  收藏  举报