65.9K
CodeProject 正在变化。 阅读更多。
Home

更简易的 .NET 设置

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (102投票s)

2012年10月14日

CPOL

23分钟阅读

viewsIcon

199987

downloadIcon

3343

创建用于在工作会话之间持久化应用程序状态数据的库。

0. 公共仓库

此项目现已托管在 Github 上,链接在此。项目名为 Jot(取自 Jot down)。这将是该项目的主仓库,并将包含最新版本的代码。请随意留下评论和问题报告,分叉仓库等等。

1. 引言

Web 应用程序和桌面应用程序都有一个常见的需求,即在工作会话之间持久化应用程序状态的一些元素。用户启动应用程序,输入一些数据,更改一些设置,移动和调整窗口大小,然后关闭应用程序。下次他们启动应用程序时,如果能记住他们输入的设置,并且 UI 元素显示为应用程序关闭之前的样子,那将是非常好的。

这要求应用程序在关闭前持久化这些数据(很可能在一个文件中),并在再次启动时应用它们。对于桌面应用程序,这些数据可能包括可移动和可调整大小的 UI 元素的位置和大小,用户输入(例如最后输入的用户名),以及应用程序设置和用户偏好。

在遇到这个需求比我愿意记住的次数更多之后,我决定花一些时间制作一个可重用库,该库可以自动化大部分持久化和应用设置的工作。整个库只有几百行代码,并且不难理解。

在本文中,我将介绍我提出的解决方案,并描述它的功能、提供的价值、如何使用以及其背后的基本思想。

2. 平台

此库可用于 WPF、Windows Forms 和 ASP.NET (WebForms/MVC) 应用程序。所需的 .NET 版本为 4.0 或更高。

3. 背后的原因和动机

在 .NET 应用程序中持久化设置的常用方法是通过内置配置 API 使用 .config 和 .settings 文件。它允许对配置数据进行类型安全访问,定义复杂的配置设置,分离用户级和应用程序级设置,运行时读写,以及通过 XML 编辑器手动修改设置。

然而,在我看来,它涉及的仪式太多了,比如为复杂设置子类化 ConfigurationSection,以及在处理带有自己设置的插件时进行修改。此外,(据我所知)生成设置类的 Visual Studio 工具不允许您干预它生成的内容(假设您想在您的设置类中实现 INotifyPropertyChanged.

但最大的问题是,以这种方式维护和使用大量设置是繁琐的。设置对象通常**不是使用数据的对象**,它们只是存储来自应用程序各地的数据。这意味着要使用这些数据,您必须编写代码,将数据从设置复制到适当的对象,然后在应用程序关闭之前的某个时间将更新的数据写回设置。

假设您的应用程序有几个可调整大小和可移动的 UI 元素,并且您希望在应用程序下次启动时记住并应用这些大小和位置。假设您有 10 个这样的 UI 元素,并且每个元素您都希望持久化 4 个属性(“Height”、“Width”、“Left”、“Top”)——总共有 40 个属性仅用于此。您可以将所有这些属性添加到您的设置文件,并编写代码将其应用于相应的 UI 元素,然后编写额外的代码在应用程序关闭之前更新设置。但是手动添加设置并编写这些代码将是相当繁琐且容易出错的。如果我们可以简单地声明我们希望跟踪某些对象的某些属性,并让它或多或少地自动处理,那会更好。

此库的主要目的就是如此——使您能够**直接在使用数据的对象上**持久化和应用数据,并以声明性方式进行,只需最少的编码(使用属性装饰属性)。

在接下来的章节中,我将演示该库的使用,并讨论其实现。

4. 术语

在本文中,我使用两个我认为可能需要解释的术语

  • 跟踪属性 - 在应用程序关闭之前保存对象的属性值,并在应用程序再次启动后识别对象并重新应用保存的值到其属性。

  • 持久化属性 - 正在被跟踪的属性

5. 用法

SettingsTracker 是协调跟踪的类。它负责将任何先前存储的数据应用于您的对象,并在适当的时候将所需对象的新数据存储到持久存储中。

创建它时,您需要告诉它如何序列化数据以及存储在哪里。这通过提供 ISerilizerIDataStore 接口的实现来完成。例如

string settingsFilePath = Path.Combine(Environment.GetFolderPath(
  Environment.SpecialFolder.ApplicationData), @"VendorName\AppName\settings.xml");
ISerializer serializer = new BinarySerializer(); //use binary serialization
IDataStore dataStore = new FileDataStore(localSettingsPath); //use a file to store data
SettingsTracker tracker = new SettingsTracker(dataStore, serializer); //create our settings tracker 

现在我们有了一个可以跟踪属性的 SettingsTracker 实例。它将使用二进制序列化来序列化数据,并将序列化的数据存储在文件中。我们应该将此实例提供给应用程序的其余部分,最好通过将其存储在 IOC 容器中,或者为了简单起见,通过公共静态属性。

现在我们只需要告诉它要跟踪哪个对象的哪些属性。有几种方法可以做到这一点。

5.1. 示例场景 1:持久化 WPF 窗口位置和大小

这个想法最好通过一个例子来说明。考虑您想要跟踪 WPF 窗口的位置、大小和 WindowState 的场景。如果您使用 .settings 文件需要做的工作显示在左侧,而使用此库实现相同效果需要编写的代码显示在右侧

A) 使用 .settings 文件


步骤 1:为主窗口的每个属性定义一个设置

步骤 2:将存储的数据应用于窗口属性

public MainWindow()
{
    InitializeComponent();

    this.Left = MySettings.Default.MainWindowLeft;
    this.Top = MySettings.Default.MainWindowTop;
    this.Width = MySettings.Default.MainWindowWidth;
    this.Height = MySettings.Default.MainWindowHeight;
    this.WindowState = 
      MySettings.Default.MainWindowWindowState;
} 

步骤 3:在窗口关闭前持久化更新的数据

protected override void OnClosed(EventArgs e)
{
    MySettings.Default.MainWindowLeft = this.Left;
    MySettings.Default.MainWindowTop = this.Top;
    MySettings.Default.MainWindowWidth = this.Width;
    MySettings.Default.MainWindowHeight = this.Height;
    MySettings.Default.MainWindowWindowState = 
               this.WindowState;

    MySettings.Default.Save();

    base.OnClosed(e);
}    

B) 使用此库


步骤 1 和 2:配置跟踪并应用状态...然后就完成了。

public MainWindow()
{
    InitializeComponent();

    //1. set up tracking for the main window
    Services.Tracker.Configure(this)
        .AddProperties<MainWindow>(w => w.Height, 
           w => w.Width, w => w.Left, 
           w => w.Top, w => w.WindowState);
        .SetKey("MainWindow")
        .Apply();
}

在此示例中,静态属性 Services.Tracker 持有一个 SettingsTracker 实例。这是为了简单起见,更好的方法是将实例保存在 IOC 容器中并从那里解析它。

   

即使对于单个窗口,选项 A 所需的工作量也相当大。很可能它会通过复制粘贴完成,并且是非常容易出错和繁琐的工作。如果我们需要在整个应用程序中跟踪许多控件,.settings 文件和智能感知将很快被一堆相似名称的属性弄得杂乱无章。

在选项 B 中,我们只需声明要跟踪主窗口的哪些属性,并为主窗口提供一个跟踪标识符,这样我们就不会将其属性与其他对象的属性混淆。调用 ApplyState 将先前持久化的数据(如果有)应用于窗口,而新数据会在应用程序关闭前自动持久化到存储中。无需编写来回复制数据的代码。

我们还可以通过在类和/或其属性上使用 [Trackable] 属性来指定要跟踪的属性列表,前提是我们控制该类的源代码。我将在下一个示例中演示这一点。

示例场景 2:持久化应用程序设置(通过属性配置跟踪)

假设您想使用以下类的实例来保存您的应用程序设置

[Trackable]//applied to class - all properties will be tracked
public class GeneralSettings
{
    public int FontSize{ get; set; }
    public Color FontColor{ get; set; }
    public string BackgroundImagePath { get; set; }
}  

以下是我们如何配置跟踪此类的实例的方法

Services.Tracker.Configure(settingsObj).AddProperties<GeneralSettings>(
                 s => s.FontSize, s => s.FontColor, s => s.BackgroundImagePath).Apply(); 

还有一种方法可以使用 [Trackable] 属性来指定要跟踪的属性列表。我将其应用于类以指定此类的所有公共属性都应被跟踪。要排除某个属性,我们将使用 [Trackable(false)] 装饰它。如果类被适当装饰了此属性,我们可以跳过使用 AddProperties 方法显式注册属性,如下所示

Services.Tracker.Configure(settingsObj).Apply();

请注意,设置类不需要继承任何特定的类,它可以子类化我们喜欢的任何类,并实现我们认为合适的接口(例如,INotifyPropertyChanged)。

为了增加酷炫度,如果使用 IOC 容器来构建我们的对象,我们可以用它来为所有它构建的对象设置跟踪。大多数 IOC 容器允许您在注入带有依赖项的对象时添加自定义步骤。我们可以利用这一点自动为实现 ITrackable(只是一个空的“标记”接口,用于标记要自动跟踪哪些对象)的任何对象添加跟踪。在这种情况下,一个类需要做的就是对其自身和/或其属性应用跟踪属性,就可以持久化其属性。其余的工作将由我们添加到 IOC 容器的扩展自动完成。

6. 优势

那么所有这些有什么好处呢?总结一下

  • 代码更少 - 您只需指定要跟踪哪个对象的哪些属性,无需编写代码在设置和其他对象之间来回复制值
  • 您不必在 .config 或 .settings 文件中显式添加新属性(也不必为您要持久化的每个对象的每个属性命名)
  • 您只需指定属性列表一次(在配置跟踪时),而不是三次(1- 在 .config 或 .settings 文件中定义设置时。2- 将数据从设置复制到其他对象时,以及 3- 将数据复制回设置时)
  • 它是声明式的 - 您可以使用属性(TrackableTrackingKey)来配置需要跟踪的内容并识别对象
  • 如果使用 IOC 容器,您几乎无需编写除了在适当属性上的属性之外的任何代码即可应用跟踪 - 更多内容请参阅“IOC 集成”一章
  • 对于 Web 应用程序,它可以使您的控制器/页面属性有状态

有关所有这些如何实现,以及如何使用和自定义的详细信息,请继续阅读...

7. 实现

与任何复杂问题一样,明智的方法是将其分解为简单的组件。我的方法使用两个基本组件:序列化和数据存储机制。这些是我的持久化库的基础。这是该库的类图

7.1. 构建块 1 - 序列化

好的,首先——为了存储任何数据,我们需要能够将数据转换为可持久化的格式。显而易见的格式候选者是字符串和字节数组。字节数组似乎是数据的最低公分母,所以我建议我们使用它。让我们声明序列化器的接口

public interface ISerializer 
{ 
    byte[] Serialize(object obj);
    object Deserialize(byte[] bytes);
}  

每个实现此接口的类都代表一种将对象转换为字节数组反之亦然的机制。现在让我们创建此接口的一个简单实现

public class BinarySerializer : ISerializer
{
    BinaryFormatter _formatter = new BinaryFormatter();
 
    public byte[] Serialize(object obj)
    {
        using (MemoryStream ms = new MemoryStream())
        {
            _formatter.Serialize(ms, obj);
            return ms.GetBuffer();
        }
    }
 
    public object Deserialize(byte[] bytes)
    {
        using (MemoryStream ms = new MemoryStream(bytes))
        {
            return _formatter.Deserialize(ms);
        }
    }
}

我们成功了。现在我们有了一个可以将对象图转换为一系列字节的类。然而,序列化是一个棘手的问题,关于这个实现,我应该指出,使用 BinaryFormatter 确实施加了某些限制:序列化的类必须用 [Serializable] 属性装饰,事件必须显式忽略(通过 [field:NonSerialized] 属性),具有循环引用的复杂对象图可能会破坏序列化。尽管如此,我在我自己的项目中已经在几种不同的场景中使用了这个实现,并且还没有遇到严重的问​​题。ISerializer 接口的其他实现可能例如使用

  • JSON(库中包含的 ISerializer 的 JSON.NET 实现)
  • SoapFormatter
  • YAML
  • protobuf.net(一个很酷的开源序列化库)
  • 基于 TypeConverter 的解决方案
  • 自定义解决方案

7.2. 构建块 2 - 数据存储

现在我们可以将一个对象转换为一系列字节,我们需要能够将序列化的数据存储到持久位置。我们可以如下声明数据存储的接口

public interface IDataStore 
{
    byte[] GetData(string identifier);
    void SetData(byte [] data, string identifier);
}    

ISerilizer 接口一样,这个接口也非常简洁。实现它的类使我们能够将(命名)二进制数据存储到持久位置并从中检索。持久化数据的候选位置可能包括

  • 文件系统(当前应用程序目录、%appsettings%、%allusersprofile%),
  • 注册表(由于访问权限问题,我不推荐此方法)
  • 数据库
  • Cookie
  • ASP.NET 会话状态(可用于向控制器和/或页面添加有状态属性)
  • ASP.NET 用户配置文件
  • 其他

我在这里使用的实现将数据存储在 XML 文件中——每个条目都作为 Base64 编码的字符串存储在带有 Id 属性的 XML 标签内。这是实现的代码

public class FileDataStore : IDataStore
{
    XDocument _document;
 
    const string ROOT_TAG = "Data";
    const string ITEM_TAG = "Item";
    const string ID_ATTRIBUTE = "Id";
 
    public string FilePath { get; private set; }
 
    public FileDataStore(string filePath)
    {
        FilePath = filePath;
 
        if (File.Exists(FilePath))
        {
            _document = XDocument.Load(FilePath);
        }
        else
        {
            _document = new XDocument();
            _document.Add(new XElement(ROOT_TAG));
        }
    }
 
    public byte[] GetData(string identifier)
    {
        XElement itemElement = GetItem(identifier);
        if (itemElement == null)
            return null;
        else
            return Convert.FromBase64String((string)itemElement.Value);
    }
 
    public void SetData(byte[] data, string identifier)
    {
        XElement itemElement = GetItem(identifier);
        if (itemElement == null)
        {
            itemElement = 
                 new XElement(ITEM_TAG, new XAttribute(ID_ATTRIBUTE, identifier));
            _document.Root.Add(itemElement);
        }
 
        itemElement.Value = Convert.ToBase64String(data);
        _document.Save(FilePath);
    }
 
    private XElement GetItem(string identifier)
    {
        return _document.Root.Elements(ITEM_TAG).SingleOrDefault(
                   el => (string)el.Attribute(ID_ATTRIBUTE) == identifier);
    }
 
    public bool ContainsKey(string identifier)
    {
        return GetItem(identifier) != null;
    }
}  

根据我们选择使用的文件位置,数据将持久化到用户特定位置或全局位置。例如,如果文件位于 %appsettings% 下的某个位置,它将是用户特定的,而如果它位于 %allusersprofile% 下,它将是所有用户的全局位置。

所以现在我们可以获取一个对象,获取它的二进制表示,并将其存储在一个持久存储中。这些就是我们所需的所有构建块。让我们继续看看如何使用它们。

* %appsettings% 和 %allusersprofile% 指的是环境变量。

7.3. ObjectStore 类

使用这两个构建块,我们可以轻松创建一个可以存储和检索整个对象的类——一个对象存储。为了区分存储中的对象,我们需要在存储/检索对象时提供一个标识符。对象存储类的代码如下所示

namespace Tracking.DataStoring
{
    public class ObjectStore : IObjectStore
    {
        IDataStore _dataStore;
        ISerializer _serializer;

        public bool CacheObjects { get; set; }

        Dictionary<string, object> _createdInstances = new Dictionary<string, object>();

        public ObjectStore(IDataStore dataStore, ISerializer serializer)
        {
            _dataStore = dataStore;
            _serializer = serializer;
            CacheObjects = true;
        }

        public void Persist(object target, string key)
        {
            _createdInstances[key] = target;
            _dataStore.SetData(_serializer.Serialize(target), key);
        }

        public bool ContainsKey(string key)
        {
            return _dataStore.ContainsKey(key);
        }

        public object Retrieve(string key)
        {
            if (!CacheObjects || !_createdInstances.ContainsKey(key))
                _createdInstances[key] = _serializer.Deserialize(_dataStore.GetData(key));
            return _createdInstances[key];
        }
    }
} 

ObjectStore 的实现非常简单。它将使用您提供的任何 ISerializerIDataStore 实现(熟悉 DI/IOC 的人会识别构造函数注入)。您可能还注意到一个字典,它用于处理对象身份(1 个键 = 1 个对象)和缓存。

因此,此类的实例可以将整个对象保存到持久位置。这本身就相当方便,但我们可以做得更多...

7.4. SettingsTracker 类

假设我们想持久化应用程序主窗口的大小和位置。为了维护其大小和位置而持久化整个窗口对象是没有意义的(即使可以做到)。相反,我们必须只跟踪特定属性的值。

顾名思义,SettingsTracker 类是协调对象属性跟踪的类。该类使用前面描述的 ObjectStore 来存储和检索跟踪属性的值。

要跟踪您的对象,您必须首先告知 SettingsTracker 实例要跟踪目标对象的哪些属性,以及何时将这些属性持久化到存储中。为此,您必须调用 Configure(object target) 方法。此方法返回一个 TrackingConfiguration 对象,您可以使用它来指定如何跟踪您的对象。

以下是配置持久化窗口大小和位置的示例

public MainWindow(SettingsTracker tracker)
{
    InitializeComponent();
 
    //configure tracking of the main window
    tracker.Configure(this)
        .AddProperties("Height", "Width", "Left", "Top", "WindowState")
        .SetKey("TheMainWindowKey")
        .SetMode(PersistModes.Automatic);
 
    //apply persisted state to the window
    tracker.ApplyState(this);
 
    
    //...
} 

在这里,我们获取跟踪窗口的配置,告诉它要持久化哪些属性,指定目标对象的标识符(键),最后指定自动模式,这意味着在应用程序关闭之前持久化属性。如果您不喜欢在指定属性时使用硬编码字符串,您可以改用 AddProperties 方法的另一个重载,如下所示

AddProperties<MainWindow>(w => w.Height, w => w.Width, w => w.Left, w => w.Top, w => w.WindowState) 

此重载分析表达式树以确定正确的属性,从而消除了对硬编码字符串的需求。

SettingsTracker 存储它创建的所有 TrackingConfiguration 对象的列表。它确保每个目标只有一个配置对象,因此每次为同一目标调用 Configure() 时,您总是会得到相同的 TrackingConfiguration 对象。

应用状态:配置要跟踪的属性后,可以通过调用 tracker.ApplyState(object target) 方法将先前持久化的状态应用于这些属性。

存储状态:在配置中,您可以将跟踪模式设置为手动或自动。如果您选择了自动跟踪模式(这是默认设置),则目标属性的值将在应用程序关闭之前(或对于 Web 应用程序,在会话结束之前)存储。如果相反,您希望在更早的时间存储它们,请使用手动模式,并在适当的时候显式调用 tracker.PersistState(target) 方法。

当持久化目标对象的属性时,设置跟踪器将

  1. 找到目标的 TrackingConfiguration
  2. 对于目标配置中指定的每个属性
    1. 通过连接目标对象类型、目标的跟踪键和属性名称来构建一个键 ([TargetObjetType]_[TargetObjectKey].[PropertyName])。
    2. 使用反射获取属性值,并使用构建的键作为标识符将其保存到存储中。

因此,对于前面示例中的窗口,PersistState 方法将存储 5 个对象到 ObjectStore,键将是

  • DemoTracking.MainWindow_TheMainWindowKey.Height
  • DemoTracking.MainWindow_TheMainWindowKey.Width
  • DemoTracking.MainWindow_TheMainWindowKey.Left
  • DemoTracking.MainWindow_TheMainWindowKey.Top
  • DemoTracking.MainWindow_TheMainWindowKey.WindowState

注意:由于应用程序中只会有一个 MainWindow 类的实例,我们实际上不必为窗口对象指定键(使用 SetKey 方法),因为它已经由其类名唯一标识。

ApplyState 方法与 PersistState 方法功能几乎相同,但数据流方向相反,从存储到对象的属性。

好的,让我们回到代码,以下是 TrackingConfiguration 类的代码

namespace Tracking
{
    public enum PersistModes
    {
        /// <summary>
        /// State is persisted automatically upon application close
        /// </summary>
        Automatic,
        /// <summary>
        /// State is persisted only upon request
        /// </summary>
        Manual
    }

    public class TrackingConfiguration
    {
        public string Key { get; set; }
        public HashSet<string> Properties { get; set; }
        public WeakReference TargetReference { get; set; }
        public PersistModes Mode { get; set; }
        public string TrackerName { get; set; }

        public TrackingConfiguration(object target)
        {
            this.TargetReference = new WeakReference(target);
            Properties = new HashSet<string>();
        }

        /// <summary>
        /// Based on Trackable and TrackingKey attributes, adds properties
        /// and setts the key.
        /// </summary>
        /// <returns></returns>
        public TrackingConfiguration AddMetaData()
        {
            PropertyInfo keyProperty = TargetReference.Target
                .GetType()
                .GetProperties()
                .SingleOrDefault(pi => pi.IsDefined(typeof(TrackingKeyAttribute), true));
            if (keyProperty != null)
                Key = keyProperty.GetValue(TargetReference.Target, null).ToString();

            //see if TrackableAttribute(true) exists on the target class
            bool isClassMarkedAsTrackable = false;
            TrackableAttribute targetClassTrackableAtt = 
              TargetReference.Target.GetType().GetCustomAttributes(
              true).OfType<TrackableAttribute>().Where(
              ta=>ta.TrackerName == TrackerName).FirstOrDefault();
            if (targetClassTrackableAtt != null && targetClassTrackableAtt.IsTrackable)
                isClassMarkedAsTrackable = true;

            //add properties that need to be tracked
            foreach (PropertyInfo pi in TargetReference.Target.GetType().GetProperties())
            {
                TrackableAttribute propTrackableAtt = 
                  pi.GetCustomAttributes(true).OfType<TrackableAttribute>(
                  ).Where(ta=>ta.TrackerName == TrackerName).FirstOrDefault();
                if (propTrackableAtt == null)
                {
                    //if the property is not marked with Trackable(true), check if the class is
                    if(isClassMarkedAsTrackable)
                        AddProperties(pi.Name);
                }
                else
                {
                    if(propTrackableAtt.IsTrackable)
                        AddProperties(pi.Name);
                }
            }
            return this;
        }

        public TrackingConfiguration AddProperties(params string[] properties)
        {
            foreach (string property in properties)
                Properties.Add(property);
            return this;
        }
        public TrackingConfiguration AddProperties(params Expression<Func<object>>[] properties)
        {
            AddProperties(properties.Select(p => ((p.Body as 
                UnaryExpression).Operand as MemberExpression).Member.Name).ToArray());
            return this;
        }
        
        public TrackingConfiguration RemoveProperties(params string[] properties)
        {
            foreach (string property in properties)
                Properties.Remove(property);
            return this;
        }
        public TrackingConfiguration RemoveProperties(params Expression<Func<object>>[] properties)
        {
            RemoveProperties(properties.Select(p => ((p.Body as 
              UnaryExpression).Operand as MemberExpression).Member.Name).ToArray());
            return this;
        }

        public TrackingConfiguration SetMode(PersistModes mode)
        {
            this.Mode = mode;
            return this;
        }

        public TrackingConfiguration SetKey(string key)
        {
            this.Key = key;
            return this;
        }
    }
}

此类别采用*方法链式*——每个方法都返回相同的 TrackingConfiguration 对象,从而方便进一步的方法调用。其实现大多直截了当。值得一提的是 AddMetaData 方法——当通过属性配置跟踪时会使用它。

请注意,配置对象存储了对目标的 WeakReference,因此它不会使其寿命超过需要。

这是 SettingsTracker 类的代码

public class SettingsTracker
{
    List<TrackingConfiguration> _configurations = new List<TrackingConfiguration>();

    public string Name { get; set; }

    IObjectStore _objectStore;
    public SettingsTracker(IObjectStore objectStore)
    {
        _objectStore = objectStore;
        WireUpAutomaticPersist();
    }

    #region automatic persisting
    protected virtual void WireUpAutomaticPersist()
    {
        if (System.Windows.Application.Current != null)//wpf
            System.Windows.Application.Current.Exit += (s, e) => { PersistAutomaticTargets(); };
        else if (System.Windows.Forms.Application.OpenForms.Count > 0)//winforms
            System.Windows.Forms.Application.ApplicationExit += (s, e) => { PersistAutomaticTargets(); };
    }

    public void PersistAutomaticTargets()
    {
        foreach (TrackingConfiguration config in _configurations.Where(
          cfg => cfg.Mode == PersistModes.Automatic && cfg.TargetReference.IsAlive))
            PersistState(config.TargetReference.Target);
    }
    #endregion

    public TrackingConfiguration Configure(object target)
    {
        TrackingConfiguration config = FindExistingConfig(target);
        if (config == null)
        {
            config = new TrackingConfiguration(target) { TrackerName = Name };
            _configurations.Add(config);
        }
        return config;
    }

    public void ApplyAllState()
    {
        foreach (TrackingConfiguration config in _configurations.Where(c=>c.TargetReference.IsAlive))
            ApplyState(config.TargetReference.Target);
    }

    public void ApplyState(object target)
    {
        TrackingConfiguration config = FindExistingConfig(target);
        Debug.Assert(config != null);

        ITrackingAware trackingAwareTarget = target as ITrackingAware;
        if ((trackingAwareTarget == null) || trackingAwareTarget.OnApplyingState(config))
        {
            foreach (string propertyName in config.Properties)
            {
                PropertyInfo property = target.GetType().GetProperty(propertyName);
                string propKey = ConstructPropertyKey(
                  target.GetType().FullName, config.Key, property.Name);
                try
                {
                    if (_objectStore.ContainsKey(propKey))
                    {
                        object storedValue = _objectStore.Retrieve(propKey);
                        property.SetValue(target, storedValue, null);
                    }
                }
                catch
                {
                    Debug.WriteLine("Applying of value '{propKey}' failed!");
                }
            }
        }
    }

    public void PersistState(object target)
    {
        TrackingConfiguration config = FindExistingConfig(target);
        Debug.Assert(config != null);

        ITrackingAware trackingAwareTarget = target as ITrackingAware;
        if ((trackingAwareTarget == null) || trackingAwareTarget.OnPersistingState(config))
        {
            foreach (string propertyName in config.Properties)
            {
                PropertyInfo property = target.GetType().GetProperty(propertyName);

                string propKey = ConstructPropertyKey(
                  target.GetType().FullName, config.Key, property.Name);
                try
                {
                    object currentValue = property.GetValue(target, null);
                    _objectStore.Persist(currentValue, propKey);
                }
                catch 
                {
                    Debug.WriteLine("Persisting of value '{propKey}' failed!");
                }
            }
        }
    }

    #region private helper methods
        
    private TrackingConfiguration FindExistingConfig(object target)
    {
        //.TargetReference.Target ---> (TrackedTarget).(WeakReferenceTarget)
        return _configurations.SingleOrDefault(cfg => cfg.TargetReference.Target == target);
    }

    //helper method for creating an identifier
    //from the object type, object key, and the propery name
    private string ConstructPropertyKey(string targetTypeName, 
                   string objectKey, string propertyName)
    {
        return string.Format("{0}_{1}.{2}", targetTypeName, objectKey, propertyName);
    }
    #endregion
}

根据应用程序类型(WinForms、WPF、ASP.NET),WireUpAutomaticPersist 方法订阅指示何时应持久化具有 PersistMode.Automatic 的目标的相应事件。

所有其他重要方法(ConfigureApplyStatePersistState)都已描述过...

7.5. 通过属性配置跟踪

配置跟踪的另一种方法是使用 TrackableTrackingKey 属性。

/// <summary>
/// If applied to a class, makes all properties trackable by default.
/// If applied to a property specifies if the property should be tracked.
/// <remarks>
/// Attributes on properties override attributes on the class.
/// </remarks>
/// </summary>
[AttributeUsage(AttributeTargets.Property | 
  AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class TrackableAttribute : Attribute
{
    public bool IsTrackable { get; set; }

    public string TrackerName { get; set; }

    public TrackableAttribute()
    {
        IsTrackable = true;
    }

    public TrackableAttribute(bool isTrackabe)
    {
        IsTrackable = isTrackabe;
    }
} 
/// <summary>
/// Marks the property as the tracking identifier for the object.
/// The property will in most cases be of type String, Guid or Int
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class TrackingKeyAttribute : Attribute
{
} 

无需为目标调用 configuration.AddProperties([属性列表]),我们可以使用 TrackableAttribute 标记目标类(或整个类)的相关属性。此外,无需调用 configuration.SetKey(“[某个键]”),我们可以用 TrackingKey 属性标记一个属性,这将使该属性表现为 ID 属性——该属性的值将是目标对象的标识符(键)。

这两个属性允许我们在类级别指定要跟踪哪些属性以及跟踪键,而无需为我们要跟踪的每个实例指定此数据。这样做还有一个好处是,如果使用 IOC 容器,它支持自动跟踪——我们只需挂钩到容器,以便在它注入对象依赖项后,如果对象实现标记接口 ITrackable,我们就调用 AddMetadataAppySettings

7.6. ITrackingAware 接口

在定义一个类时,并非总是可以为属性添加属性。例如,当我们子类化 System.Windows.Window 时,我们无法控制其中定义的属性(除非它们是虚拟的),因为我们无法控制 Window 类的源代码,所以我们无法用属性装饰它们。在这种情况下,我们可以实现 ITrackingAware 接口,它看起来像这样

/// <summary>
/// Allows the object that is being tracked to customize
/// its persitence
/// </summary>
public interface ITrackingAware : ITrackable
{
    /// <summary>
    /// Called before applying persisted state to the object.
    /// </summary>
    /// <param name="configuration"></param>
    /// <returns>Return false to cancel applying state</returns>
    bool OnApplyingState(TrackingConfiguration configuration);
    /// <summary>
    /// Called after state aplied.
    /// </summary>
    /// <returns></returns>
    void OnAppliedState();

    /// <summary>
    /// Called before persisting object state.
    /// </summary>
    /// <param name="configuration"></param>
    /// <returns>Return false to cancel persisting state</returns>
    bool OnPersistingState(TrackingConfiguration configuration);
    /// <summary>
    /// Called after state persisted.
    /// </summary>
    /// <param name="configuration"></param>
    /// <returns></returns>
    void OnPersistedState();
}

此接口允许我们在应用和持久化状态之前修改跟踪配置,甚至可以取消其中任何一个。这对于 WindowsForms 也很有用,在 WindowsForms 中,最小化时窗体具有虚假的大小和位置——在这种情况下,我们可以取消持久化最小化的窗口。

7.7. IOC 集成

现在到了最酷的部分... 在应用程序中使用 IOC 容器(Unity/Castle Windsor/Ninject/Lin Fu 等)时,许多对象都是由 IOC 容器创建或构建(注入它们的依赖项)的。那么为什么不让容器自动配置跟踪并向它构建的所有可跟踪对象应用状态呢!

这样,如果您的对象将由容器构建,您只需执行以下操作即可使属性持久化

  1. 确保定义属性的类实现了空标记接口 ITrackable 并用 [Trackable] 装饰该属性,- 或 -
  2. 以适当的方式实现 ITrackingAware 接口

ITrackable 接口没有成员,仅用作标记,以便让 IOC 扩展知道您希望自动跟踪具有它的对象。我选择使用接口而不是属性,因为检查属性的存在比检查接口稍慢。

注意:ITrackingAware 已经继承自 ITrackable

到目前为止,我已经在 Unity 和 Ninject 中使用了这种方法,但我怀疑在其他 IOC 容器中实现它应该不难。以下是自动向对象添加跟踪的 UnityContainerExtension 的代码

namespace Tracking
{
    /// <summary>
    /// Marker interface for classes that want their tracking to be handled 
    /// by the IOC container.
    /// <remarks>
    /// Checking if a class implements an interface is faster that checking
    /// if its decorated with an attribute.
    /// </remarks>
    /// </summary>
    public interface ITrackable 
    {
    }

    /// <summary>
    /// Unity extension for adding (attribute based) state tracking to creted objects
    /// </summary>
    public class TrackingExtension : UnityContainerExtension
    {
        class TrackingStrategy : BuilderStrategy
        {
            IUnityContainer _container;
            public TrackingStrategy(IUnityContainer container)
            {
                _container = container;
            }

            public override void PostBuildUp(IBuilderContext context)
            {
                base.PostBuildUp(context);
                ITrackable autoTracked = context.Existing as ITrackable;
                if (autoTracked != null)
                {
                    IEnumerable<SettingsTracker> trackers = 
                       _container.ResolveAll<SettingsTracker>();
                    foreach (SettingsTracker tracker in trackers)
                    {
                        tracker.Configure(autoTracked).AddMetaData(
                           ).SetMode(PersistModes.Automatic);
                        tracker.ApplyState(autoTracked);
                    }
                }
            }
        }

        protected override void Initialize()
        {
            Context.Strategies.Add(
              new TrackingStrategy(Container), UnityBuildStage.Creation);
        }
    }
}

以下是如何配置 Unity 容器以使用此扩展添加跟踪支持的方法

IUnityContainer _container = new UnityContainer();
string localSettingsFilePath = Path.Combine(Environment.GetFolderPath(
  Environment.SpecialFolder.ApplicationData), "testsettingswithIOC.xml");
 
_container.RegisterType<IDataStore, FileDataStore>(
  new ContainerControlledLifetimeManager(), new InjectionConstructor(localSettingsFilePath));
_container.RegisterType<ISerializer, BinarySerializer>(new ContainerControlledLifetimeManager());
_container.RegisterType<IObjectStore, ObjectStore>(new ContainerControlledLifetimeManager());
_container.RegisterType<SettingsTracker>(new ContainerControlledLifetimeManager());
 
_container.AddExtension(new TrackingExtension());    

该库还包含专门用于 WPF (WPFTrackingExtension) 和 WinForms (WinFormsTrackingExtension) 的派生 TrackingExtension 类,当添加这些类时,它们会自动分别为 Windows (WPF) 和 Forms (WinForms) 配置跟踪。这样,UnityContainer 解析的所有 Windows/Forms 都将毫不费力地跟踪其大小和位置(除了向 UnityContainer 注册适当的 TrackingExtension)。

8. Web 应用程序怎么样?

在 Web 应用程序中,对象的生命周期确实非常短。它们在服务器开始处理请求时创建,并在响应发送后立即丢弃。除了手动在服务器上存储数据(例如通过使用 Session 存储或用户配置文件)外,服务器不保留任何应用程序状态。相反,(任何)状态在每次请求-响应(在查询字符串、表单数据、cookie 等中)中在客户端和服务器之间来回传递。

例如,“Session”对象可用于维护状态,但它很笨拙,并且编译器无法确保其中数据的类型和名称安全。

然而,在 Web 应用程序中使用此库允许拥有 ASP.NET 页面和 MVC 控制器,它们的属性似乎在回发之间“存活”。根据所使用的 IDataStore 实现,数据可以存储在 Session 状态、ASP.NET 用户配置文件或其他地方。我们不需要做任何其他事情,只需用 [Trackable] 属性装饰所需的属性,并确保页面或控制器是使用带有跟踪管理扩展的 IOC 容器构建的。使用 IOC 解析页面和控制器可以通过自定义 ControllerFactory(对于 MVC)或自定义 IHttpModule(对于常规 ASP.NET)来完成——我已在两种 ASP.NET 版本中都包含了带有重要部分注释的演示应用程序。那么让我们看看如何使用此库来处理页面访问次数的计数(MVC 示例)。

a) 直接使用 Session
[HandleError]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        uint numberOfVisits = 0;
        //1. Get the value from session
        //if present: no compile time checking
        //of type or identifier
        if (Session["numberOfVisits"] != null)
            numberOfVisits = (uint)Session["numberOfVisits"];

        //2. do something with the value... 
        ViewData["NumberOfVisits_User"] = numberOfVisits;

        //3. increment the number of visits
        numberOfVisits++;

        //4. store it in the Session state
        Session["SomeIdentifier"] = numberOfVisits;

        return View();
    }
}
b) 使用此库
[HandleError]
public class HomeController : Controller, ITrackable
{
    [Trackable]
    public uint NumberOfVisits { get; set; }

    public ActionResult Index()
    {
        //no need to do anything
        //to fetch or save NumberOfVisits 

        //1. Do something with the value...
        ViewData["NumberOfVisits"] = NumberOfVisits;
        //2. increment the number of users
        NumberOfVisits++;

        return View();
    }
}

在这种情况下,选项 B 有几个优点

  • 简单性(只需将 Trackable 属性应用于所需属性)
  • 名称安全(保存/检索时无需担心会话存储中数据的命名)
  • 类型安全(从会话存储中检索数据时无需进行类型转换)

8.1. 在 ASP.NET WebForms 中配置跟踪

为了在 ASP.NET (WebForms) 中启用此行为,我创建了一个自定义 IHttpModule,这样我就可以在页面处理之前和之后做一些事情。该模块在其构造函数中引用了 IUnityContainer 并执行以下操作

  1. 将跟踪扩展添加到 IOC 容器(因此容器创建或注入的每个对象都会被跟踪,如果它实现了 ITrackable 标记接口)
  2. 在 HttpHandler(ASP.NET 页面)开始执行之前,它使用 IOC 容器向其注入依赖项(并向其以及在此过程中创建的任何其他对象应用跟踪)
  3. 处理程序(ASP.NET 页面)处理完成后,它会调用容器中注册的所有 SettingsTracker 上的 PersistAutomaticTargets

这是 http 模块的代码

namespace Tracking.Unity.ASPNET
{
    public class TrackingModule : IHttpModule
    {
        IUnityContainer _container;
        public TrackingModule(IUnityContainer container)
        {
            _container = container;
            _container.AddExtension(new TrackingExtension());
        }
 
        public void Dispose()
        {
        }
 
        public void Init(HttpApplication context)
        {
            context.PreRequestHandlerExecute += 
              new EventHandler(context_PreRequestHandlerExecute);
            context.PostRequestHandlerExecute += 
              new EventHandler(context_PostRequestHandlerExecute);
        }
 
        void context_PreRequestHandlerExecute(object sender, EventArgs e)
        {
            if (HttpContext.Current.Handler is IRequiresSessionState || 
                 HttpContext.Current.Handler is IReadOnlySessionState)
            {
                object page = HttpContext.Current.Handler;
                _container.BuildUp(page.GetType(), page);
            }
        }
 
        void context_PostRequestHandlerExecute(object sender, EventArgs e)
        {
            if (HttpContext.Current.Handler is IRequiresSessionState || 
                    HttpContext.Current.Handler is IReadOnlySessionState)
            {
                //named trackers
                foreach (SettingsTracker tracker in _container.ResolveAll<SettingsTracker>())
                    tracker.PersistAutomaticTargets();
 
                //unnamed tracker
                if(_container.IsRegistered<SettingsTracker>())
                    _container.Resolve<SettingsTracker>().PersistAutomaticTargets();
            }
        }
    }
} 

由于模块在其构造函数中需要对 IUnityContainer 的引用,因此需要在代码中创建它(而不是在 app.config 中)。这必须在 global.asax 文件的 Init() 方法中完成,如下所示

namespace WebApplication1
{
    public class Global : System.Web.HttpApplication
    {
        public static UnityContainer _uc = new UnityContainer();
        static IHttpModule trackingModule = new TrackingModule(_uc);

        public override void Init()
        {
            base.Init();

            //Register services in the IOC container
            //...

            //register appropriate SettingsTrackers
            //i use a factory method instead of a single
            // instance so each session can have it's own instance
            //so they don't interfere with each other
            _uc.RegisterType<SettingsTracker>(new SessionLifetimeManager(), 
              new InjectionFactory(c => new SettingsTracker(new ObjectStore(
              new ProfileStore("TrackingData"), 
              new BinarySerializer()) { CacheObjects = false })));

            //initialize the tracking module
            trackingModule.Init(this);
        }
    }
}

8.2. 在 ASP.NET MVC 中配置跟踪

在 MVC 中,控制器不是处理程序,因此 HttpModule 方法不适用。相反,可以使用自定义控制器工厂设置依赖注入和跟踪。我已经在库中包含了一个,它看起来像这样

namespace Tracking.Unity.ASPNET
{
    public class TrackingControllerFactory : DefaultControllerFactory
    {
        IUnityContainer _container;
        public TrackingControllerFactory(IUnityContainer container)
        {
            _container = container;
            _container.AddExtension(new TrackingExtension());

            HttpContext.Current.ApplicationInstance.PostRequestHandlerExecute += 
                   new EventHandler(ApplicationInstance_PostRequestHandlerExecute);
        }

        void ApplicationInstance_PostRequestHandlerExecute(object sender, EventArgs e)
        {
                //named trackers
                foreach (SettingsTracker tracker in _container.ResolveAll<SettingsTracker>())
                    tracker.PersistAutomaticTargets();

                //unnamed tracker
                if (_container.IsRegistered<SettingsTracker>())
                    _container.Resolve<SettingsTracker>().PersistAutomaticTargets();
        }

        #region IControllerFactory Members

        public override IController CreateController(
            System.Web.Routing.RequestContext requestContext, string controllerName)
        {
            IController controller = base.CreateController(requestContext, controllerName);
            _container.BuildUp(controller);
            return controller;
        }

        #endregion
    }
} 

控制器工厂也需要在 global.asaxInit() 方法中进行设置,因为它正在订阅 PostRequestHandlerExecute 事件,该事件只能在 Init 期间工作。global.asax 文件可能如下所示

namespace MvcApplication1
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801
    public class MvcApplication : System.Web.HttpApplication
    {
        static UnityContainer _uc = new UnityContainer();

        public override void Init()
        {
            base.Init();

            //register appropriate SettingsTrackers
            _uc.RegisterType<SettingsTracker>("USER", 
              new RequestLifetimeManager(), new InjectionFactory(container => 
              new SettingsTracker(new ObjectStore(new ProfileStore("TrackingData"), 
              new JsonSerializer())) { Name = "USER" }));
            _uc.RegisterType<SettingsTracker>("SESSION", 
              new SessionLifetimeManager(), new InjectionFactory(container => 
              new SettingsTracker(new ObjectStore(new SessionStore(), 
              new JsonSerializer())) { Name = "SESSION" }));

            //IMPORTANT: use the TrackingControllerFactory to create controllers
            //so we can inject dependencies into them and apply tracking
            ControllerBuilder.Current.SetControllerFactory(new TrackingControllerFactory(_uc));
        }

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", 
                      id = UrlParameter.Optional } // Parameter defaults
            );
        }

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RegisterRoutes(RouteTable.Routes);
        }
    }
}

现在我们所需要做的就是用 [Trackable] 装饰我们希望持久化的属性,并在控制器中实现 ITrackable 接口。您可能已经注意到我在这里注册了不止一个设置跟踪器。这把我带到了最后一个有趣的观点...

9. 多个跟踪器(命名跟踪器)

有时您需要存储一些数据,例如在用户级别,而其他数据在机器级别,或者可能在会话级别。在这种情况下,我们可以创建多个跟踪器,为每个跟踪器命名,在 IOC 容器中按名称注册它们,并在 Trackable 属性中指定跟踪器名称。MVC 演示中显示了一个示例,但下面是指定跟踪器名称的样子

[Trackable(TrackerName = "USER")]
public uint NumberOfVisits_User { get; set; }

[Trackable(TrackerName = "SESSION")]
public uint NumberOfVisits_Session { get; set; } 

我在 ASP.NET WebForms 演示应用程序中包含了一个使用多个跟踪器的示例,其中一些属性按用户级别跟踪,另一些属性按会话级别跟踪。

10. 演示应用程序

在桌面演示应用程序中,我使用了跟踪库来持久化 UI 状态,以及持久化应用程序设置(不使用标准的 .NET 配置 API)。请注意,我在我的一个设置类中实现 INotifyPropertyChanged 没有任何问题。如果我的应用程序支持插件,我也不会在允许插件拥有自己的设置方面遇到任何问题。在演示中,有一个应用程序使用 Unity IOC 容器,另一个不使用。

我还包括了一个 ASP.NET WebForms 应用程序和一个 MVC 应用程序,其中包含一个使用多个跟踪器的示例。这些应用程序使用带有 aspnetdb.mdf 文件的 ASP.NET 用户配置文件来存储用户数据。根据您安装的 SQL Server,您可能需要调整 web.config 中的连接字符串才能使演示正常工作。

11. 结论

保存设置并将其应用于相关对象的工作涉及大量数据的来回复制,这可能相当单调且容易出错。在本文中,我旨在提出一种更具声明性的方法,其中您只需指定需要持久化的**内容**和**时间**,并自动处理复制(“**如何**”)。这种方法大大减少了工作量、代码和重复。

 

历史

 

  • 更新 2013-06-18:添加了 WinForms 示例。
  • 更新 2013-06-12:Web 应用中的使用,JSON 序列化,多个(命名)跟踪器。
  • 更新 2014-11-10 库已发布到 Github
© . All rights reserved.