WebForm —— 页面状态自动加载和保存(下)
很久之前写完了上、中两篇,因为各种原因吧,到现在也没有完成下篇,心里一直有些愧疚。好了,废话不说了,把下篇补上,也是我用到现在的代码。
第一步,新建一个类,并且让类从 BasePage 继承。
第二步,重写 BasePage 类的两个虚方法:GetCacheData 和 SaveCacheData ,分别处理数据的 Load 和 Save 。
第三步,保存这个类,并让页面的后台类(系统默认继承自 Page)继承自这个新类就可以了。
好了,步骤理解之后,原理在上中两篇说的差不多了,剩下的看代码就行了,有问题给我留言就 OK 。
首先是中篇中提到的的 AutoSaveAttribute 特性类:
using System; using System.Diagnostics; namespace Lenic.Web { /// <summary> /// 自动保存属性,配合 BasePage 能够实现 Web 页面后台代码类字段或属性的自动保存和加载。 /// </summary> [DebuggerStepThrough] [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)] public class AutoSaveAttribute : Attribute { /// <summary> /// 初始化创建一个 <see cref="AutoSaveAttribute"/> 类的实例,使得具有该属性的类的属性或字段具有自动保存的特性。 /// </summary> public AutoSaveAttribute() { } } }
然后是核心处理逻辑 BasePage 虚基类:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Web.UI; namespace Lenic.Web { /// <summary> /// 提供了 Web 页面自动保存属性处理的基类 /// </summary> [DebuggerStepThrough] public abstract class BasePage : Page { #region Reload Fields And Properties /// <summary> /// 引发 <see cref="E:System.Web.UI.Control.Load"/> 事件。 /// </summary> /// <param name="e">包含事件数据的 <see cref="T:System.EventArgs"/> 对象。</param> protected override void OnLoad(EventArgs e) { // 初始化当前用户控件的缓冲字典 InitCacheDic(); if (Page.IsPostBack) { // 获得缓冲数据列表 var list = GetCacheData(); // 自动加载 AutoSave 属性保存的值 int index = 0; foreach (MemberInfo info in CacheDic[CurrType]) { if (info.MemberType == MemberTypes.Property) { PropertyInfo pi = info as PropertyInfo; object value = list[index]; if (value != null) pi.SetValue(this, value, null); } else if (info.MemberType == MemberTypes.Field) { FieldInfo fi = info as FieldInfo; object value = list[index]; fi.SetValue(this, value); } index++; } } base.OnLoad(e); } #endregion #region Save Fields And Properties /// <summary> /// 在这里实现属性的自动保存。 /// </summary> protected override object SaveViewState() { // 初始化当前用户控件的缓冲字典 InitCacheDic(); // 初始化要保存的属性值列表 List<object> list = new List<object>(); foreach (MemberInfo info in CacheDic[CurrType]) { if (info.MemberType == MemberTypes.Property) { PropertyInfo pi = info as PropertyInfo; list.Add(pi.GetValue(this, null)); } else if (info.MemberType == MemberTypes.Field) { FieldInfo fi = info as FieldInfo; list.Add(fi.GetValue(this)); } } // 保存更改 SaveCacheData(list); return base.SaveViewState(); } #endregion #region Business Properties /// <summary> /// 用户控件类型及自动保存属性成员缓冲字典 /// </summary> protected static Dictionary<Type, MemberInfo[]> CacheDic = null; /// <summary> /// 当前页面的类型 /// </summary> protected Type CurrType = null; /// <summary> /// 获得成员列表的绑定标识. /// </summary> private static readonly BindingFlags Flag; /// <summary> /// 初始化 <see cref="BasePage"/> 类. /// </summary> static BasePage() { CacheDic = new Dictionary<Type, MemberInfo[]>(); Flag = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.FlattenHierarchy; } /// <summary> /// 初始化当前页面的缓冲字典 /// </summary> private void InitCacheDic() { // 获得当前实例类型 CurrType = GetType(); MemberInfo[] mems = null; if (!CacheDic.TryGetValue(CurrType, out mems)) { var list = CurrType.GetMembers(Flag) .Where(p => Attribute.IsDefined(p, typeof(AutoSaveAttribute), false)) .ToArray(); CacheDic[CurrType] = list; } } #endregion #region Data Fetch /// <summary> /// 获得缓存的数据。 /// </summary> /// <returns>重获的数据。</returns> protected abstract List<object> GetCacheData(); /// <summary> /// 保存需要缓存的数据。 /// </summary> /// <param name="data">需要保存的数据数组。</param> protected abstract void SaveCacheData(List<object> data); #endregion } }
其次是对 GetCacheData 方法和 SaveCacheData 的实现:
private ICacheList _currCache = null; private ICacheList CurrCache { get { if (_currCache == null) { _currCache = SqliteCacheList.NewInstance(CurrUser.SessionID, CurrUser.ID, CurrType.FullName); _currCache.LoadPageData(); } return _currCache; } } protected override List<object> GetCacheData() { var data = CurrCache.Where(p => p.PageID == CurrType.FullName) .Select(p => p.Data == null ? null : p.Data.DeserializeFromByte<object>()) .ToList(); return data; } protected override void SaveCacheData(List<object> data) { data.ForEach((p, i) => CurrCache[i].Data = p == null ? null : p.SerializeToByte()); CurrCache.Save(); }
这里用到了一些自定义的扩展方法:
List<T> 类的 ForEach 方法添加了第二个参数 i 表示当前索引值。
DeserializeFromByte<T> 是 byte[] 字节数组反序列化为 T 类型对象的扩展。
SerializeToByte 是将当前对象序列化为 byte[] 字节数组的扩展。
这三个方法应该问题都不大,百度一下就能找到类似的实现,这里就不再贴代码了。
再次是缓存的具体实现代码。下面的代码是我个人实现的方法,每个人的想法可能都不同,权当抛砖引玉了:
观察仔细的童鞋,可以看到 ICacheList 接口,这就是我自定义的一个接口:
using System.Collections.Generic; namespace Lenic.Web.Caches { /// <summary> /// 缓存列表 /// </summary> public interface ICacheList : IEnumerable<CacheItem> { /// <summary> /// 【自动新建】获得或设置缓存项。 /// </summary> /// <param name="i">项的索引</param> /// <returns>缓存项</returns> CacheItem this[int i] { get; set; } /// <summary> /// 从数据库中加载数据 /// </summary> /// <typeparam name="T">数据的类型</typeparam> /// <param name="dataId">数据标识</param> /// <returns>还原的原始数据</returns> T LoadData<T>(string dataId); /// <summary> /// 从数据库中加载页面数据 /// </summary> /// <returns>加载后的列表</returns> ICacheList LoadPageData(); /// <summary> /// 持久化数据变化 /// </summary> void Save(); } }
其中 IEnumerable<CacheItem> 中的 CacheItem 表示缓存项的虚基类:
using System; using System.Diagnostics; using Lenic.Data; using Lenic.Extensions; namespace Lenic.Web.Caches { /// <summary> /// 缓存项 /// </summary> [Serializable] [DebuggerStepThrough] public abstract class CacheItem { #region Instance /// <summary> /// 初始化创建一个 <paramref name="CacheItem"/> 类的对象。 /// </summary> public CacheItem() { MarkNew(); } /// <summary> /// 初始化创建一个 <paramref name="CacheItem"/> 类的对象。 /// </summary> /// <param name="isFetched"><c>true</c> 表示是从数据库中填充获得的; 否则返回 <c>false</c> 。</param> public CacheItem(bool isFetched) { if (isFetched) MarkFetched(); else MarkNew(); } /// <summary> /// 初始化创建一个 <paramref name="CacheItem"/> 类的对象。 /// </summary> /// <param name="sessionID">会话标识</param> /// <param name="userID">用户标识</param> /// <param name="pageID">页面标识</param> /// <param name="dataID">数据标识</param> /// <returns>获得或新建的一个 <paramref name="CacheItem"/> 类的对象</returns> public CacheItem(string sessionID, string userID, string pageID, string dataID) : this() { SessionID = sessionID; UserID = userID; PageID = pageID; DataID = dataID; } #endregion #region Business Properties /// <summary> /// 获得或设置当前会话标识。 /// </summary> public string SessionID { get; set; } /// <summary> /// 获得当前用户标识。 /// </summary> public string UserID { get; set; } /// <summary> /// 获得或设置当前页面标识。 /// </summary> public string PageID { get; set; } /// <summary> /// 获得或设置当前数据标识。 /// </summary> public string DataID { get; set; } private byte[] _data = null; /// <summary> /// 获得或设置当前数据对象数据。 /// </summary> public byte[] Data { get { return _data; } set { _data = value; IsDirty = true; } } /// <summary> /// 获得最近一次的修改时间 /// </summary> public DateTime LastChanged { get; set; } /// <summary> /// 获得当前数据对象。 /// </summary> public object DataObject { get { return Data == null ? null : Data.DeserializeFromByte<object>(); } } #endregion #region Mark Instance /// <summary> /// 获得当前实例是否是新建、未保存到数据库中。 /// </summary> public bool IsNew { get; protected set; } /// <summary> /// 获得当前实例是否是否是从数据库中检索并填充的。 /// </summary> public bool IsFetched { get; protected set; } /// <summary> /// 获得一个值, 通过该值指示当前实例对象是否被修改过。 /// </summary> /// <value><c>true</c> 表示被修改过; 否则返回 <c>false</c> 。</value> public bool IsDirty { get; protected set; } /// <summary> /// 获得当前实例是否已经标识为删除。 /// </summary> public bool IsDeleted { get; protected set; } /// <summary> /// 标识当前对象是新建、未保存到数据库中。 /// </summary> /// <returns>更改后的自身。</returns> public CacheItem MarkNew() { IsDirty = false; IsNew = true; IsFetched = false; IsDeleted = false; return this; } /// <summary> /// 标识当前对象为从数据库中检索并填充的。 /// </summary> /// <returns>更新后的自身。</returns> public CacheItem MarkFetched() { IsDirty = false; IsNew = false; IsFetched = true; IsDeleted = false; return this; } /// <summary> /// 标识当前实例对象需要在保存时删除。 /// </summary> /// <returns>修改后的自身。</returns> public CacheItem MarkDeleted() { IsDirty = true; IsDeleted = true; return this; } #endregion #region Equal public override bool Equals(object obj) { if (obj == null || typeof(CacheItem) != obj.GetType()) return false; var target = obj as CacheItem; if (this.SessionID == target.SessionID && this.UserID == target.UserID && this.PageID == target.PageID && this.DataID == target.DataID) return true; return base.Equals(obj); } public override int GetHashCode() { int hash = SessionID.GetHashCode(); hash ^= UserID.GetHashCode(); hash ^= PageID.GetHashCode(); hash ^= DataID.GetHashCode(); return hash; } #endregion #region Data Operater /// <summary> /// 持久化到数据库中。 /// </summary> /// <param name="t">数据库操作实例对象</param> /// <returns>影响的行数。</returns> public abstract int Save(IDataAccesser t); #endregion } }
在项目中我用 Sqlite 写了一个实现,具体代码如下:
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using Lenic.Data; using Lenic.Data.Extensions; using System.Linq; using Lenic.Extensions; namespace Lenic.Web.Caches.Sqlite { /// <summary> /// 页面 ViewState 数据库缓冲 /// </summary> [Serializable] [DebuggerStepThrough] public class SqliteCacheList : ICacheList { private List<SqliteCacheItem> list = new List<SqliteCacheItem>(); #region DbHelper /// <summary> /// 缓存数据库连接字符串 /// </summary> public static string ConnectionString = @"Data Source=|DataDirectory|\PageCache.dll"; /// <summary> /// 数据库访问实例对象 /// </summary> private static IDataAccesser DB = new DbHelper(DbProviders.SQLiteProvider, ConnectionString); /// <summary> /// 查询缓冲表是否在数据库中存在, 返回一个 <paramref name="System.Int64"/> 类型的值表示找到的个数. /// </summary> private const string SelectExists = "SELECT COUNT(*) AS COUNTS FROM SQLITE_MASTER WHERE TYPE = 'table' AND NAME = 'TB_Page'"; /// <summary> /// 创建缓冲表语句. /// </summary> private const string CreateTable = @"CREATE TABLE [TB_Page] ( [SessionID] nvarchar(50) NOT NULL, [UserID] nvarchar(50) NOT NULL, [PageID] nvarchar(50) NOT NULL, [DataID] nvarchar(50) NOT NULL, [Data] blob, [LastChanged] timestamp NOT NULL )"; /// <summary> /// 删除缓冲表语句. /// </summary> private const string DropTable = "Drop Table TB_Page"; /// <summary> /// 收缩数据库语句. /// </summary> private const string ShrinkDB = "Vacuum"; /// <summary> /// 清除用户之前的记录文本. /// </summary> private const string ClearPreviousText = "DELETE FROM [TB_Page] WHERE [UserID] = '{0}'"; /// <summary> /// 清除指定 SessionID 指定用户的的记录文本. /// </summary> private const string ClearSessionText = "DELETE FROM [TB_Page] WHERE [SessionID] = {0} AND [UserID] = '{1}'"; /// <summary> /// 【页面缓存】0 = 会话标识 AND 1 = 用户标识 AND 2 = 页面标识 /// </summary> private const string SelectPageSql = "SELECT * FROM [TB_Page] WHERE [SessionID] = '{0}' AND [UserID] = '{1}' AND [PageID] = '{2}'"; /// <summary> /// 【变量数据】0 = 会话标识 AND 1 = 用户标识 AND 2 = 页面标识 AND 3 = 数据标识 /// </summary> private const string SelectSingleDataSql = "SELECT [Data] FROM [TB_Page] WHERE [SessionID] = '{0}' AND [UserID] = '{1}' AND [PageID] = '{2}' AND [DataID] = '{3}'"; #endregion #region Business Properties /// <summary> /// 获得会话标识 /// </summary> public string SessionID { get; private set; } /// <summary> /// 获得用户标识 /// </summary> public string UserID { get; private set; } /// <summary> /// 获得页面标识 /// </summary> public string PageID { get; private set; } #endregion #region New Instance private SqliteCacheList() { } /// <summary> /// 新建一个列表对象 /// </summary> public static SqliteCacheList NewInstance(string sessionID, string userID, string pageID) { return new SqliteCacheList { SessionID = sessionID, UserID = userID, PageID = pageID, }; } /// <summary> /// 【自动新建】获得或设置缓存项。 /// </summary> /// <value></value> /// <returns>缓存项</returns> public CacheItem this[int i] { get { var item = list.ElementAtOrDefault(i); if (item == null) { item = new SqliteCacheItem(SessionID, UserID, PageID, i.ToString()); list.Add(item); } return item; } set { if (i >= list.Count) list.Add((SqliteCacheItem)value); else { list.RemoveAt(i); list.Insert(i, (SqliteCacheItem)value); } } } #endregion #region IEnumerable 成员 /// <summary> /// 返回一个循环访问集合的枚举数。 /// </summary> /// <returns>可用于循环访问集合的 System.Collections.Generic.IEnumeratorlt;SqliteCacheItemgt;。</returns> public IEnumerator<CacheItem> GetEnumerator() { var data = list.GetEnumerator(); while (data.MoveNext()) { yield return (CacheItem)data.Current; } } /// <summary> /// 返回一个循环访问集合的枚举数。 /// </summary> /// <returns>可用于循环访问集合的 System.Collections.Generic.IEnumeratorlt;SqliteCacheItemgt;。</returns> IEnumerator<CacheItem> IEnumerable<CacheItem>.GetEnumerator() { return this.GetEnumerator(); } /// <summary> /// 返回一个循环访问集合的枚举数。 /// </summary> /// <returns>可用于循环访问集合的 System.Collections.IEnumerator。</returns> IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } #endregion #region Data Operater /// <summary> /// 初始化缓冲数据库。 /// </summary> /// <param name="deleteTable">如果设置为 <c>true</c> 标识先删除 Table 再重建。</param> public static void Init(bool deleteTable) { long count = 0; using (DataReaderEx con = DB.Read(SelectExists)) { count = con.Field<long>("COUNTS"); } if (count > 0 && deleteTable) DB.Write(DropTable); if (count == 0) DB.Write(CreateTable); DB.Write(ShrinkDB); } /// <summary> /// 从数据库中清除用户旧的记录, 立即生效! /// </summary> /// <param name="userID">用户标识.</param> public static void ClearPrevious(string userId) { DB.Write(ClearPreviousText.With(userId)); } /// <summary> /// 持久化数据变化 /// </summary> public void Save() { using (var con = DbProviders.SQLiteProvider.CreateConnection()) { con.ConnectionString = ConnectionString; con.Open(); var transaction = con.BeginTransaction(); var t = new TransactionHelper(transaction); try { foreach (var item in this) { int count = item.Save(t); if (count == 0) throw new DatabaseException("数据库操作失败"); } t.Commit(); } catch (Exception e) { t.Rollback(); throw e; } } } /// <summary> /// 从数据库中加载页面数据 /// </summary> /// <param name="sessionId">会话标识</param> /// <param name="userId">用户标识</param> /// <param name="pageId">页面标识</param> /// <returns>加载后的列表</returns> public ICacheList LoadPageData() { if (SessionID.IsNullOrEmptyTrim()) throw new ApplicationException("会话标识不能为空"); if (UserID.IsNullOrEmptyTrim()) throw new ApplicationException("用户标识不能为空"); if (PageID.IsNullOrEmptyTrim()) throw new ApplicationException("页面标识不能为空"); using (var con = DbProviders.SQLiteProvider.CreateConnection()) { con.ConnectionString = ConnectionString; con.Open(); var transaction = con.BeginTransaction(); var t = new TransactionHelper(transaction); try { using (DataReaderEx dr = t.Read(SelectPageSql.With(SessionID, UserID, PageID))) { list = dr.ToList<SqliteCacheItem>(p => new SqliteCacheItem(true) { SessionID = p.Field<string>("SessionID"), UserID = p.Field<string>("UserID"), PageID = p.Field<string>("PageID"), DataID = p.Field<string>("DataID"), Data = p.Field<byte[]>("Data"), LastChanged = p.Field<DateTime>("LastChanged"), }); } } finally { t.Rollback(); } } return this; } /// <summary> /// 从数据库中加载数据 /// </summary> /// <typeparam name="T">数据的类型</typeparam> /// <param name="sessionId">会话标识</param> /// <param name="userId">用户标识</param> /// <param name="dataId">数据标识</param> /// <returns>还原的原始数据</returns> public T LoadData<T>(string dataId) { if (SessionID.IsNullOrEmptyTrim()) throw new ApplicationException("会话标识不能为空"); if (UserID.IsNullOrEmptyTrim()) throw new ApplicationException("用户标识不能为空"); var data = DB.GetValue(SelectSingleDataSql.With(SessionID, UserID, "Lenic.Global", dataId)) .DirectTo<byte[]>(); if (data == null) return default(T); return data.DeserializeFromByte<T>(); } #endregion } }
下面是缓存项的实现类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; using Lenic.Data; using Lenic.Extensions; namespace Lenic.Web.Caches.Sqlite { /// <summary> /// Sqlite 缓存项 /// </summary> [Serializable] [DebuggerStepThrough] public class SqliteCacheItem : CacheItem { #region Instance /// <summary> /// 初始化创建一个 <paramref name="SqliteCacheItem"/> 类的对象。 /// </summary> public SqliteCacheItem() { MarkNew(); } /// <summary> /// 初始化创建一个 <paramref name="SqliteCacheItem"/> 类的对象。 /// </summary> /// <param name="isFetched"><c>true</c> 表示是从数据库中填充获得的; 否则返回 <c>false</c> 。</param> public SqliteCacheItem(bool isFetched) { if (isFetched) MarkFetched(); else MarkNew(); } /// <summary> /// 初始化创建一个 <paramref name="SqliteCacheItem"/> 类的对象。 /// </summary> /// <param name="sessionID">会话标识</param> /// <param name="userID">用户标识</param> /// <param name="pageID">页面标识</param> /// <param name="dataID">数据标识</param> /// <returns>获得或新建的一个 <paramref name="SqliteCacheItem"/> 类的对象</returns> public SqliteCacheItem(string sessionID, string userID, string pageID, string dataID) : this() { SessionID = sessionID; UserID = userID; PageID = pageID; DataID = dataID; } #endregion #region Command Text /// <summary> /// 【插入】0 = 会话标识 AND 1 = 用户标识 AND 2 = 页面标识 AND 3 = 数据标识 AND 4 = 插入序号 /// </summary> internal const string InsertText = "INSERT INTO [TB_Page]([SessionID], [UserID], [PageID], [DataID], [Data], [LastChanged]) Values('{0}', '{1}', '{2}', '{3}', @p{4}, datetime());"; /// <summary> /// 【更新】0 = 会话标识 AND 1 = 用户标识 AND 2 = 页面标识 AND 3 = 数据标识 AND 4 = 更新序号 /// </summary> internal const string UpdateText = "UPDATE [TB_Page] SET [Data] = @p{4}, [LastChanged] = datetime() WHERE [SessionID] = '{0}' AND [UserID] = '{1}' AND [PageID] = '{2}' AND [DataID] = '{3}'"; /// <summary> /// 【删除】0 = 会话标识 AND 1 = 用户标识 AND 2 = 页面标识 AND 3 = 数据标识 /// </summary> internal const string DeleteText = "Delete From [TB_Page] WHERE [SessionID] = '{0}' AND [UserID] = '{1}' AND [PageID] = '{2}' AND [DataID] = '{3}'"; #endregion #region Data Operater /// <summary> /// 持久化到数据库中。 /// </summary> /// <param name="t">数据库操作实例对象</param> /// <returns>影响的行数。</returns> public override int Save(IDataAccesser t) { if (IsNew && IsDeleted) return -1; if (!IsDirty) return -1; if (IsDeleted) return t.WriteD(DeleteText.With(SessionID, UserID, PageID, DataID)); if (IsNew) return t.WriteD(InsertText.With(SessionID, UserID, PageID, DataID, 0), Data); if (IsFetched) return t.WriteD(UpdateText.With(SessionID, UserID, PageID, DataID, 0), Data); return 0; } #endregion } }
就到这里吧,能拿出来的都拿出来了。后面的代码也包含了一些自定义方法,我略作解释:
IDataAccesser 接口操作数据库,包含下面的方法,具体靠 DbHelper 和 TransactionHelper 实现,我就不写了,你应该能写出来一个实现类。很简单的!
/// <summary> /// 数据库操作接口 /// </summary> public interface IDataAccesser { /// <summary> /// 创建一个新的数据库命令对象。 /// </summary> /// <returns>一个新的数据库命令对象。</returns> DbCommand NewCommand(); /// <summary> /// 执行查询, 并返回查询所返回的结果集中第一行的第一列. 所有其他的列和行将被忽略. /// </summary> /// <param name="cmd">查询命令实例对象.</param> /// <returns>结果集中第一行的第一列.</returns> object GetValue(DbCommand cmd); /// <summary> /// 从数据库中查询并返回结果集(DataSet 类型)。 /// </summary> /// <param name="cmd">查询命令实例对象。</param> /// <returns>查询结果集。</returns> DataSet Query(DbCommand cmd); /// <summary> /// 从数据库中查询并返回一个 <paramref name="System.Data.Common.DbDataReader"/> 类型的实例对象。 /// </summary> /// <param name="cmd">查询命令实例对象。</param> /// <returns>查询结果集。</returns> DbDataReader Read(DbCommand cmd); /// <summary> /// 执行数据库操作, 并返回影响的行数。 /// </summary> /// <param name="cmd">执行命令实例对象。</param> /// <returns>影响的行数。</returns> int Write(DbCommand cmd); }
DataReaderEx 是 DbDataReader 的一个实现类,用修饰模式实现,这里你可以把其当成 IDataReader 接口来看待。
IsNullOrEmptyTrim 是对 String 类 IsNullOrEmpty 方法的封装,同时增加了对 Trim 方法处理后的判断。
DirectTo 是对类型强转的包装,等于 (T)obj 。
With 是对 String.Format 的封装。