C#中的MVC
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));//返回首页 }