G
N
I
D
A
O
L

更简单的 .NET 设置

0. 公共仓库

这个项目现在托管在 Github 上,链接在这里。项目名称为 Jot(如 Jot down)。这将是该项目的主要存储库,并将包含代码的最新版本。请随时发表评论和发布报告,分叉存储库和所有好东西。

1. 引言

Web 和桌面应用程序的常见要求是在工作会话之间保留应用程序状态的某些元素。用户启动应用程序,输入一些数据,更改一些设置,移动窗口并调整窗口大小,然后关闭应用程序。下次他们启动应用程序时,如果他们输入的设置被记住,并且 UI 元素显示为应用程序关闭之前的样子,那就太好了。

这要求应用程序在关闭之前保留此数据(很可能在文件中),并在再次启动时应用它。对于桌面应用程序,此数据可能包括可移动和可调整大小的 UI 元素的位置和大小、用户输入(例如,上次输入的用户名)以及应用程序设置和用户首选项。

在遇到这个要求的次数比我记不清的要多之后,我决定花一些时间制作一个可重用的库,它可以自动执行保存和应用设置的大部分工作。整个库只有几百行代码,并不难理解。

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

2. 平台

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

3. 这背后的原因和动机

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

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

但最大的问题是,以这种方式维护和使用大量设置是乏味的。设置对象通常不是使用数据的对象,它们只是存储来自整个应用程序的数据。这意味着,若要使用此数据,必须编写代码,将数据从设置复制到相应的对象,然后在应用程序关闭前的某个时间将更新的数据再次写回设置。

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

这个库的主要目的就是 - 使你能够直接在使用它的对象上持久化和应用数据,并以最少的编码以声明方式这样做(用属性修饰属性)。

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

4. 术语

在这篇文章中,我使用了两个我认为可能需要解释的术语:

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

  • persistent 属性 - 正在跟踪的属性

5. 用法

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

创建数据时,需要告诉它如何序列化数据以及存储数据的位置。这是通过向它提供 and 接口的实现来完成的。例如:ISerilizerIDataStore

C#
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 

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

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

5.1. 示例方案 1:持久保存 WPF 窗口位置和大小

这个想法最好用一个例子来说明。请考虑要跟踪 WPF 窗口的位置、大小和方案。如果使用 .settings 文件,则需要执行的工作显示在左侧,而使用此库编写的代码将达到相同的效果,则显示在右侧:WindowState

A) 使用 .settings 文件


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

图像 1

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

C#
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:在窗口关闭之前保留更新的数据

C#
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:配置跟踪并应用状态...我们完成了。

C#
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();
}

在此示例中,static 属性保存一个实例。为简单起见,更好的方法是将实例保存在 IOC 容器中并从那里解析它。Services.TrackerSettingsTracker

   

选项A所需的工作量相当大,即使是单个窗口也是如此。最有可能的是,它将使用复制粘贴来完成,并且非常容易出错且繁琐的工作。如果我们必须在整个应用程序中跟踪许多控件,则 .settings 文件和 intellisense 很快就会被类似名称的属性所包围。

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

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

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

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

C#
[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; }
}  

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

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

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

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

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

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

6. 优势

那么这一切有什么好处呢?总结一下:

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

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

7. 实施

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

图像 2

7.1. 构建块 1 - 序列化

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

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

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

C#
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(包含在库中的 JSON.NET 实现)ISerializer
  • SoapFormatter
  • YAML公司
  • protobuf.net(一个很酷的开源序列化库)
  • TypeConverter基于解决方案
  • 定制解决方案

7.2. 构建块 2 - DataStore

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

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

与界面一样,此界面也相当小。实现它的类使我们能够在持久性位置存储和检索(命名)二进制数据。要保留数据的候选项位可能包括:ISerilizer

  • 文件系统(当前应用程序目录、%AppSettings%、%AllUsersProfile%)、
  • 注册表(由于访问权限问题,我不建议这样做)
  • 数据库
  • 饼干
  • ASP.NET 会话状态(可用于向控制器和/或页面添加有状态属性)
  • ASP.NET 用户配置文件
  • 其他

我在这里使用的实现将数据存储在一个 XML 文件中 - 每个条目都作为 Base64 编码的字符串存储在具有 Id 属性的 XML 标记中。下面是实现的代码:

C#
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 类

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

C#
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];
        }
    }
} 

的实现非常简单。它将使用任何实现,你给它(熟悉 DI/IOC 的人会识别构造函数注入)。您可能已经注意到的另一件事是字典,它用于处理对象标识(1 个键 = 1 个对象)和缓存。ObjectStoreISerializerIDataStore

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

7.4. SettingsTracker 类

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

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

若要跟踪对象,必须首先告诉实例要跟踪目标的哪些属性,以及何时将这些属性保存到存储中。若要实现此目的,必须调用该方法。此方法返回一个对象,用于指定如何跟踪对象。SettingsTrackerConfigure(object target)TrackingConfiguration

下面是一个示例,说明如何配置持久化窗口的大小和位置:

C#
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

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

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

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

应用状态:配置要跟踪的属性后,可以通过调用该方法将任何以前保留的状态应用于这些属性。tracker.ApplyState(object target)

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

保留目标对象的属性时,设置跟踪器将:

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

因此,对于上一个示例中的窗口,该方法将存储 5 个对象,键为:PersistStateObjectStore

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

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

该方法执行与执行几乎相同的操作,但将数据以相反的方向移动,从存储到对象的属性。ApplyStatePersistState

好了,让我们回到代码,下面是类的代码:TrackingConfiguration

C#
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;
        }
    }
}

此类使用方法链 每个方法返回相同的对象,从而便于进一步的方法调用。实现大多很简单。值得一提的是方法 - 当通过属性配置跟踪时使用它。TrackingConfigurationAddMetaData

请注意,配置对象将 a 存储到目标,因此它不会使其生存时间超过所需的时间。WeakReference

这是该类的代码:SettingsTracker

C#
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),该方法订阅相应的事件,该事件指示何时应保留 with 的目标。WireUpAutomaticPersistPersistMode.Automatic

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

7.5. 配置按属性跟踪

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

C#
/// <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
{
} 

我们可以用 .此外,我们可以用该属性标记一个属性,而不是调用 ,这将导致该属性的行为类似于 ID 属性——此属性的值将是目标对象的标识符(键)。configuration.AddProperties([list of properties])TrackableAttributeconfiguration.SetKey(“[some key]”)TrackingKey

这两个属性允许我们在类级别指定要跟踪的属性和跟踪键,而不必为要跟踪的每个实例指定此数据。这样做的另一个好处是,如果我们使用 IOC 容器,它可以启用自动跟踪 - 我们只需挂接到容器中,以便在它注入对我们调用的对象的依赖项后,如果对象实现标记接口。AddMetadataAppySettingsITrackable

7.6. ITrackingAware接口

定义类时,并不总是可以使用属性来修饰属性。例如,当我们子类化时,我们无法控制其中定义的属性(除非它们是虚拟的),因为我们无法控制类的源代码,因此我们无法用属性修饰它们。在这种情况下,我们可以实现如下所示的接口:System.Windows.WindowWindowITrackingAware

C#
/// <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 来说也很方便,其中表单在最小化时具有虚假的大小和位置 - 在这种情况下,我们可以取消保留最小化的窗口。

7.7. IOC集成

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

这样,如果你的对象将由容器构建,你需要做的就是使属性持久化:

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

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

注意:已继承自 。ITrackingAware ITrackable

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

C#
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 容器以添加跟踪支持的方式:

C#
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/窗体都将毫不费力地跟踪其大小和位置(除了向 UnityContainer 注册适当的 TrackingExtension)。

8. Web 应用程序呢?

在 Web 应用程序中,对象的生命周期确实很短。它们是在服务器开始处理请求时创建的,并在发送响应后立即丢弃。除了在服务器上手动存储数据(例如,通过使用会话存储或用户配置文件)之外,服务器不会保留任何应用程序状态。取而代之的是,(任何)状态从客户端传递到服务器,然后随着每个请求响应(在查询字符串、表单数据、cookie 等内)再次传递回来。

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

但是,在 Web 应用程序中使用此库允许拥有 ASP.NET 页面和 MVC 控制器,其属性似乎在回发之间“存活”。根据所使用的实现,数据可以存储在会话状态、ASP.NET 用户配置文件或其他位置。我们不需要做任何其他事情,只需使用 [Trackable] 属性修饰所需的属性,并确保页面或控制器是使用 IOC 容器构建的,该容器带有用于管理跟踪的扩展。使用 IOC 解析页面和控制器可以通过自定义 ControllerFactory(用于 MVC)或自定义(用于常规 ASP.NET)来完成——我已经为这两种风格的 ASP.NET 提供了演示应用程序,并对重要部分进行了评论。因此,让我们看看如何使用此库来计算对页面的访问次数(MVC 示例)。IDataStoreIHttpModule

a) 直接使用 Session
C#
[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) 使用此库
C#
[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
  • 名称安全(保存/检索时无需担心在会话存储中命名数据)
  • 类型 Safety(从会话存储中检索数据时无需强制转换)

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

为了在 ASP.NET (WebForms) 中启用此行为,我创建了一个自定义项,以便我可以在处理页面之前和之后执行操作。该模块在其构造函数中接受引用并执行以下操作:IHttpModuleIUnityContainer

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

这是 http 模块的代码:

C#
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();
            }
        }
    }
} 

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

C#
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 方法不适用。相反,可以使用自定义控制器工厂设置依赖关系注入和跟踪。我在库中包含了一个,这是它的样子:

C#
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.asax 的方法中设置控制器工厂,因为它订阅的事件只能在 Init 期间工作。global.asax 文件可能如下所示:Init()PostRequestHandlerExecute

C#
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 容器中注册它们,并在属性中指定跟踪器名称。MVC 演示中显示了一个示例,但指定跟踪器名称如下所示:Trackable

C#
[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)。请注意,我在我的一个设置类中实现没有问题。如果我的应用程序启用了插件,我也不会有任何问题,允许插件有自己的设置。在演示中,有一个应用使用了 Unity IOC 容器,另一个应用没有。INotifyPropertyChanged

我还包含了一个 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 上

许可证

本文以及任何相关的源代码和文件均根据 The Code Project Open License (CPOL) 获得许可

 

作者
软件开发人员(高级) Recro-Net
克罗地亚 克罗地亚
自 2005 年以来,我一直是一名软件开发人员,主要从事 .NET 方面的工作。目前在克罗地亚萨格勒布生活和工作。我于 2006 年在萨格勒布电气工程和计算机科学学院获得计算机科学硕士学位。
posted @ 2024-06-13 19:34  firespeed  阅读(9)  评论(0编辑  收藏  举报