高效的服务器端视图状态持久化
通过减小页面下载量来提高 ASP.NET 网站的性能。
引言
视图状态 (View State) 是 ASP.NET 网页使用的一种机制,用于持久化特定 ASP.NET 网页本身、单个控件、对象以及存在于该网页上的数据的状态。视图状态是一把双刃剑,正确使用它可以帮助开发人员构建功能丰富且强大的 Web 应用程序,这些应用程序似乎克服了 Web 的无状态性。
随着 Web 应用程序复杂性的增长,屏幕上添加的控件越来越多,需要持久化的数据也越来越多……视图状态也随之增长。这是不可避免的。即使我们非常小心地确保只有需要视图状态的项目才开启它,视图状态的大小也可能达到几 KB。对于自定义的内联网/外联网应用程序,视图状态大小达到 50KB 到 100KB 并不罕见。
背景
Scott Mitchell 多年前在 MSDN 上发表了一篇关于视图状态的出色文章。既然他已经做了出色的工作,我将不再赘述视图状态的详细信息,但会提及几点:
- 好东西太多了…… 默认情况下,您添加到 ASP.NET 网页的每个控件都启用了视图状态。每个按钮、标签、网格、下拉列表……一切。这意味着页面上的每个控件都会使视图状态的大小增加。
- 视图状态会持久化在 HTML 标记中。是的,各位。页面上使用视图状态的每个控件都会使其视图状态变大……也会使整个页面大小变大。
- 视图状态双向传输。由于视图状态由 ASP.NET 使用,而 ASP.NET 只存在于服务器上,因此发送到浏览器的视图状态必须回传到服务器才能使用。是的,这包括使用 AJAX 的部分页面更新。
- 视图状态仅在服务器上使用。我知道我在第 3 点提到过这一点,但在此重申……视图状态仅在服务器上由 ASP.NET 运行时使用……
要求
- 视图状态需要持久化在服务器上。
- 视图状态持久化机制需要由特定的用户会话来识别。
- 持久化的视图状态对象不得永久保留。
- 持久化的视图状态应能够按页面启用和禁用。
- 应该可以使用不同的持久化机制。
- 不应修改页面开发和结构。
控件适配器来帮忙
由于 ASP.NET 的 Page
对象本质上是一个控件(虽然是*这个*控件……但无论如何它仍然是一个控件),我们可以通过简单的控件适配器修改其行为。通常,在谈论控件适配器时,开发社区普遍会想到 CSS 控件适配器。
MSDN 对控件适配器的定义如下:控件适配器是组件,它们会重写 Control
类在其执行生命周期中的某些方法和事件,以允许进行特定于浏览器或标记的处理。 .NET Framework 会为每个客户端请求将一个派生的控件适配器映射到一个 Control
对象。
我发现了 Robert Boedigheimer 这篇出色的文章,关于在 ASP.NET 中使用 SessionPageStatePersister
进行服务器端视图状态。我当时就知道这是我要走的路……尽管使用 SessionPageStatePersister
会遇到内存资源有限的相同问题……所以这并不能算一个一劳永逸的解决方案……
我的解决方案实际上包含两部分。第一部分是 PageStateAdapter
控件适配器,第二部分是自定义持久化机制 CachePageStatePersister
。
PageStateAdapter
为了满足我的需求 #4 和 #5,我决定采用简单的属性方案,开发人员可以通过在页面类定义顶部放置一个属性来决定使用不同的视图状态持久化方案。属性类和支持的枚举都定义在 PageStateAdapter
类中。
public enum StateStorageTypes { Default, Cache, Session, InPage }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class PageViewStateStorageAttribute : Attribute
{
private readonly StateStorageTypes storageType = StateStorageTypes.Default;
public PageViewStateStorageAttribute(StateStorageTypes stateStorageType)
{
storageType = stateStorageType;
}
internal StateStorageTypes StorageType
{
get { return storageType; }
}
}
现在,PageStateAdapter
只需要实现虚拟方法 GetStatePersister
。
public override PageStatePersister GetStatePersister()
{
PageViewStateStorageAttribute psa =
Attribute.GetCustomAttribute(Page.GetType(),
typeof(PageViewStateStorageAttribute), true) as
PageViewStateStorageAttribute ??
new PageViewStateStorageAttribute(StateStorageTypes.Default);
PageStatePersister psp;
switch (psa.StorageType)
{
case StateStorageTypes.Session:
psp = new SessionPageStatePersister(Page);
break;
case StateStorageTypes.InPage:
psp = new HiddenFieldPageStatePersister(Page);
break;
default:
psp = new CachePageStatePersister(Page);
break;
}
return psp;
}
如果开发人员希望覆盖(现在是默认的)CachePageStatePersister
的视图状态持久化方法,他们可以通过将一个简单的属性应用于页面类声明来实现。
[PageStateAdapter.PageViewStateStorage(PageStateAdapter.StateStorageTypes.InPage)]
public partial class ViewStateInPage : System.Web.UI.Page
CachePageStatePersister
CachePageStatePersister
继承自 PageStatePersister
。因此,它只需要实现两个虚拟方法 Load()
和 Save()
。
public override void Save()
{
if (ViewState != null || ControlState != null)
{
if (Page.Session == null)
throw new InvalidOperationException(
"Session is required for CachePageStatePersister (SessionID -> Key)");
string vsKey;
string cacheFile;
// create a unique cache file and key based on this user's
// session and page instance (time)
if (!Page.IsPostBack)
{
string sessionId = Page.Session.SessionID;
string pageUrl = Page.Request.Path;
vsKey = string.Format("{0}{1}_{2}_{3}", VSPREFIX, pageUrl, sessionId,
DateTime.Now.Ticks);
string cachePath = Page.MapPath(CACHEFOLDER);
if (!Directory.Exists(cachePath))
Directory.CreateDirectory(cachePath);
cacheFile = Path.Combine(cachePath, BuildFileName());
}
// get our vs key from the page, re use it, and the cache
// file (pulled from page.cache)
else
{
vsKey = Page.Request.Form[VSKEY];
if (string.IsNullOrEmpty(vsKey)) throw new ViewStateException();
cacheFile = Page.Cache[vsKey] as string;
if (string.IsNullOrEmpty(cacheFile)) throw new ViewStateException();
}
IStateFormatter frmt = StateFormatter;
string state = frmt.Serialize(new Pair(ViewState, ControlState));
using (StreamWriter sw = File.CreateText(cacheFile))
sw.Write(state);
Page.Cache.Add(vsKey, cacheFile, null, DateTime.Now.AddMinutes(
Page.Session.Timeout),
Cache.NoSlidingExpiration, CacheItemPriority.Low,
ViewStateCacheRemoveCallback);
Page.ClientScript.RegisterHiddenField(VSKEY, vsKey);
}
}
在我们的 Save
方法中,您可以看到我们做了很多事情……首先是生成一个唯一的视图状态键(如果是新页面请求),该键基于正在请求的页面、用户的会话 ID 和以刻度表示的时间。然后,我们将视图状态和控件状态都序列化到一个物理文件中(在本例中,我们将文件存储在 ~/App_Data/Cache 目录下)。文件创建后,我们将视图状态键和文件路径存储在 Page.Cache
中,并将视图状态键保存到页面中的一个隐藏字段。
如果页面是通过 POST
请求的,那么我们就知道我们已经为这个页面实例拥有了一个唯一的键。我们从页面中提取该键,并从缓存中提取文件路径,然后重用这些设置来持久化视图状态和控件状态。
因此,我们通过仅将文件路径存储在缓存中来考虑有限的服务器资源,通过仅将唯一键存储在页面中(而不是整个视图状态)来考虑下载页面大小,并通过使用 Page.Cache
对象将所有内容关联起来,我们赋予了持久化的视图状态文件生命周期。请注意,在 Page.Cache.Add()
方法的最后一部分,我们将 ViewStateCacheRemoveCallback
定义为项目从缓存中移除时的回调方法。
public static void ViewStateCacheRemoveCallback(string key,
object value, CacheItemRemovedReason reason)
{
string cacheFile = value as string;
if (!string.IsNullOrEmpty(cacheFile))
if (File.Exists(cacheFile))
File.Delete(cacheFile);
}
当缓存进行回调时,它会传递缓存对象中包含的内容,我们知道那是持久化视图状态对象的文件的路径。收到回调后,我们只需要删除物理文件。
Load
方法基本上与 Save
方法的工作方式相反……
public override void Load()
{
if (!Page.IsPostBack) return;
// We don't want to load up anything if this is an inital request
string vsKey = Page.Request.Form[VSKEY];
// Sanity Checks
if (string.IsNullOrEmpty(vsKey)) throw new ViewStateException();
if (!vsKey.StartsWith(VSPREFIX)) throw new ViewStateException();
IStateFormatter frmt = StateFormatter;
string state = string.Empty;
string fileName = Page.Cache[vsKey] as string;
if (!string.IsNullOrEmpty(fileName))
if (File.Exists(fileName))
using (StreamReader sr = File.OpenText(fileName))
state = sr.ReadToEnd();
if (string.IsNullOrEmpty(state)) return;
Pair statePair = frmt.Deserialize(state) as Pair;
if (statePair == null) return;
ViewState = statePair.First;
ControlState = statePair.Second;
}
这里的一些要点是……如果页面不是作为回发工作的,我们就不想从持久化中加载视图状态。这解决了用户加载过时数据的问题。Load()
方法从页面获取其视图状态键,然后使用该视图状态键从 Page.Cache
中获取持久化视图状态文件的路径。然后读取文件,将其反序列化为 Pair
对象,然后从 Pair
对象加载 ViewState
和 ControlState
。
浏览器文件
最后一点是,将 PageStateAdapter
与位于 App_Browsers 目录下的简单 .browser 文件一起使用。
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.Page" adapterType="PageStateAdapter" />
</controlAdapters>
</browser>
</browsers>
结论
视图状态虽然是一个强大的工具,但它可能会很快导致用户无法充分享受您的网站。通过利用内置的 ASP.NET 技术,如缓存和控件适配器,我们可以有效地、准确地在服务器上持久化视图状态,而不是将其流式传输给用户。使用本文所述的 PageStateAdapter
使我们的开发人员能够在不重新设计整个页面框架的情况下,充分利用我们现有的 ASP.NET 技能集,包括我们对 ASP.NET AJAX 的大量使用;它确实是一个即插即用的解决方案。我们的 PageStateAdapter
方法可以非常轻松地进行修改,以便在多主机 Web 环境中将视图状态文件持久化到公共位置,并且 Page.Cache
可以替换为该相同环境中的公共机制。我们在培训环境中使用这项技术,经历了实际负载和极限测试负载,并取得了非常令人印象深刻的结果。
在我们的代码库中,我们还在全局 Application_Start
和 Application_End
事件中添加了删除可能存在的任何 .cache 文件。这可以处理服务器重启时可能存在的任何遗留文件,或者在缓存移除回调中可能遗漏的文件。
关注点
当我们开始在服务器上持久化视图状态时,我们将整个视图状态都塞进了 Page.Cache
。它在我们的工作站和开发服务器上运行良好。在培训服务器上也运行良好……直到培训服务器出现负载。100 个用户和三个小时后,服务器崩溃了,因为内存不足而彻底瘫痪。要点:Page.Cache
……是在内存中的。
历史
- 2008-08-06 - 修改为使持久化器感知页面实例:高效服务器端视图状态持久化,Two Dot DOH!!
- 2008-08-05 - 根据我的博客文章创建了初始文章:高效服务器端视图状态持久化