.NET MVC TempData、ViewData、ViewBag
.NET技术交流群:337901356 ,欢迎您的加入!
ViewData和ViewBag主要用于将数据从控制器中传递到视图中去,ViewData本身就是一个字典。以KeyValue的形式存取值。ViewData的Value类型是Object,也就是可以将任意类型的值存储到ViewData中去,平时我们都在控制器中直接使用ViewData.本质上ViewData只是Controller父类ControllerBase中的一个属性,其类型是ViewDataDictionary,因为我们在自己的Controller中并未定义一个叫做ViewData的属性,也就是说当我们访问在某个类的属性或者方法中所访问的某个方法或者属性中没有找到时,我们就要想到这个属性或者方法是否在父类中已经定义了,这个对于一个新手来说往往是容易忽略的,TempData是用于解决在不同的的Action方法之间跳转的时候的数据传递。这里不同的Action可以是同一个Controller下的不同的Action之间,也可以是不同Controller的Action之间。有些人说,利用Session不是也可以实现吗?是的,没错,不过仔细的去看下微软的Mvc源码,你会发现,其实TempData中的数据的维护也是用到了Session的。
ViewData
我们很经常看到这样,
public ActionResult Index() { //从数据库中读取产品列表 List<Product> productList = db.Products.ToList(); //这个时候productList这个集合对象将被传递到视图页中去. //如果此时,在视图页面中使用@model List<Product> 声名,你会发现在视图页面中,直接访问View //注意,每个视图页面在网站第一次被请求时,都会被编译成一个对应的 //一个类,这些视图类都会被编译到一个临时的程序集中去,这个临时的程序集的位置,可以通过在视图页面中编写代码@this.GetType().Assembly.Locaiton //的方式来查看其位置,实际上就是在C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\vs的某个目录下。 //这个类直接或者间接继承自:System.Web.Mvc.WebViewPage类. return View(productList); }
细心的查看View()方法的源码:
protected internal virtual ViewResult View(string viewName, string masterName, object model) { if (model != null) { //看到了没有,其实就是直接将我们想要传递到视图页的数据,保存到了父类ControllerBase的ViewData属性对象的一个名为Model的属性中去了。 //然后在将Controller中的ViewData的引用传递给视图页面类的实例对象上的ViewData属性,这样就能将在控制器的Action中往ViewData中设置的值传递到视图中去了,也就是我们上面的代码还可以改为 //ViewData.Model=productList;return View();就行了。 base.ViewData.Model = model; } return new ViewResult { ViewName = viewName, MasterName = masterName, ViewData = base.ViewData, TempData = base.TempData, ViewEngineCollection = this.ViewEngineCollection }; }
需要注意的是:ViewData中的数据只能传递到当前这个Action所要去加载的视图页面中去,而不能跨Action传输。
ViewBag
ViewBag,其实内部真正存储数据的还是ViewData,也就是说,ViewData和ViewBag的数据是共享的,通过ViewData设置的数据,可以通过ViewBag访问,通过ViewBag设置的数据可以通过
ViewData访问。
看看ControllerBase中的ViewBag属性的源码:
[Dynamic] public object ViewBag { [return: Dynamic] get { Func<ViewDataDictionary> viewDataThunk = null; if (this._dynamicViewDataDictionary == null) { if (viewDataThunk == null) { viewDataThunk = () => this.ViewData; } //viewDataThunk是个lambda表达式,返回ViewData.也就是DynamicViewDataDictionary内部还是用的是ViewData. this._dynamicViewDataDictionary = new DynamicViewDataDictionary(viewDataThunk); } return this._dynamicViewDataDictionary; } }
以下是DynamicViewDataDictionary中的两个方法。
public override bool TryGetMember(GetMemberBinder binder, out object result) {
//看到重点了吧。 result = this.ViewData[binder.Name]; return true; } public override bool TrySetMember(SetMemberBinder binder, object value) { this.ViewData[binder.Name] = value; return true; }
TempData
我们需要知道的是,.NET Mvc中最终处理来自于浏览器端的请求的是一个MvcHandler的类,这个类实现了IHttpHandler,而IHttpHandler中定义了一个ProccessRequest方法,和WebForm
不一样的是,Controller对象的创建是在MvcHandler中来完成的。从MvcHanlder的ProccessRequest方法中开始追踪,我们会发现以下代码:
protected internal virtual void ProcessRequest(HttpContextBase httpContext) { IController controller; IControllerFactory factory; this.ProcessRequestInit(httpContext, out controller, out factory); try { controller.Execute(this.RequestContext); } finally { factory.ReleaseController(controller); } }
再看看controller.Execute的源码(因为controller变量是IController接口类型,所以要查看实现了IController的类的Execute方法)发现是ControllerBase实现了该接口中的Execute方法,再看看,ControllerBase中的Execute方法,发现调用了自己的ExecuteCore方法,发现ExecuteCore是一个abstract方法,没有方法体,然后我们从其子类Controller中看到了重写了其父类ControllerBase中的ExecuteCore方法,Controller->ExecuteCore方法如下:
protected override void ExecuteCore() { //这句代码表示在调用目标Action之前,去Session中加载对应的来自于上个Action
//中保存的传递过来的数据。 this.PossiblyLoadTempData(); try { string requiredString = this.RouteData.GetRequiredString("action"); if (!this.ActionInvoker.InvokeAction(base.ControllerContext, requiredString)) { this.HandleUnknownAction(requiredString); } } finally { this.PossiblySaveTempData(); } }
我们在看看PosiblyLoadTempData这个方法中的代码,很明显,这个方法就是Controller中的.
internal void PossiblyLoadTempData() { //这里说明了,只有当前被请求的不是子Action的时候才会去加载对应的TempData数据,从Session中。 if (!base.ControllerContext.IsChildAction) { //TempData的类型是TempDataDictionary,我们看看这个类中的Load方法,查看TempDataDictionary.Load方法,我们发现其实真正去加载的是一个实现了ITempDataProvider接口的某个类的实例对象去加载的。
//其实就是SessionStateTempDataProvider,找到这个类,查看其LoadTempData方法。 base.TempData.Load(base.ControllerContext, this.TempDataProvider); } }
下面是SessionStateTempDataProvider->LoadTempData方法源码:
// Methods public virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext) { HttpSessionStateBase session = controllerContext.HttpContext.Session; if (session != null) { //真相大白了,其实我们从TempData中取数据时,还是从一个key为__ControllerTempData的Session中取出来的,也就是说TempData只是一个临时的数据保存的地方,
//最终在调用Action完毕后,框架自动把在Action中往TempData中设置的值保存到Session中去,然后跳转到下个Action并在这个Action执行之前,又从Session中取出来,
//通过as Dictionary让我们也知道了,其实TempData中保存数据的就是一个普通的字典而已,这就是为什么TempData能在不同的请求之间保存数据,同时也说明了为什么能在多个不同的Action之间无限的进行
//数据传递。 Dictionary<string, object> dictionary = session["__ControllerTempData"] as Dictionary<string, object>; if (dictionary != null) { session.Remove("__ControllerTempData"); return dictionary; } } return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); }
下面是SessionStateTempDataProvider的SaveTempData方法
public virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values) { if (controllerContext == null) { throw new ArgumentNullException("controllerContext"); } HttpSessionStateBase session = controllerContext.HttpContext.Session; bool flag = (values != null) && (values.Count > 0); if (session == null) { if (flag) { throw new InvalidOperationException(MvcResources.SessionStateTempDataProvider_SessionStateDisabled); } } else if (flag) { session["__ControllerTempData"] = values; } else if (session["__ControllerTempData"] != null) { session.Remove("__ControllerTempData"); } }
再回头看看Controller类中的ExecuteCore方法,其实就是在浏览器发出请求之后,调用控制器的某个Action之前,先会去Session中尝试找Key为__ControllerTempData的Session["__ControllerTempData"]是否为null,如果不为null,那么就将其取出,并且转换为Dictionary<string,object>类型,并且存储到ControllerBase父类中的TempData属性对象里的内部属性_data中去,
然后我们在Action中或者视图中取出TempData的值的时候,就是从这个内部字典_data中取值的,在执行完action并且执行完视图页面的代码之后(this.ActionInvoker.InvokeAction(base.ControllerContext, requiredString这句代码会去找到对应的视图页面类,并且去执行,),最后一个步骤,就是去将TempData中内部字典的内容保存到Session中去,因为是最后执行,所以,在视图页面中保存到TempData的值也会被保存起来,以供下次使用,也就是说TempData是跨请求的,但是你会发现如果经过了两次请求,也就是从浏览器中输入两次,你会发现只能取一次,为什么呢,看下面这个TempDataDictionary的Save方法。
public void Save(ControllerContext controllerContext, ITempDataProvider tempDataProvider) {
//也就是保存到Session之前,会去先看看这些key是否是通过我们手动TempData[""]的方式设置进去的
//(之所以这种判断,是因为在使用TempData["key"]=value的时候,索引器的set中包含了一句,this._initialKeys.add(key)的方法)
//如果不是,则不会去再次保存,这就是为什么你在第二个请求的Action中只能取一次的原因了。
//这里说的,是指第三次刷新第二次请求的那个Action时,已经无法访问到第一次请求存进去的TempData中的值了,因为第二次请求的时候,因为这个key没有在_initialKeys和_retainKeys这两个HashSet中。
//如果要继续保留,请在使用TempData中的数据之前,注意:是之前哦,调用Keep()或者Keep(string key)方法。 this._data.RemoveFromDictionary<string, object, TempDataDictionary>(delegate (KeyValuePair<string, object> entry, TempDataDictionary tempData) { string item = entry.Key; return !tempData._initialKeys.Contains(item) && !tempData._retainedKeys.Contains(item); }, this); tempDataProvider.SaveTempData(controllerContext, this._data); }
为什么会在使用一次之后,就不会在保存回Session中去了呢,如果,还不够清楚,还可以看看,TempData的索引器,可以发现get下有一个关键代码:
public object this[string key] { get { object obj2; if (this.TryGetValue(key, out obj2)) {
//原来这边在我们取数据的时候,将将key从_initialKeys中移除了,当TempData.Save方法被调用时,发现_data字典中的我们取数据的那个key不在这个HashSet中,所以就不会被保存了。
//如果你希望取数据了之后,又希望还能传递到下一个action用的话,那么请使用Peek方法。 this._initialKeys.Remove(key); return obj2; } return null; } set { this._data[key] = value;
//这句说明了,为什么我们使用TempData["key"]=value设置的值,可以留到下次使用的原因。 this._initialKeys.Add(key); } }