可轻松保存属性数据的 .NET 库
用 [RegSave] 将你的属性保存到注册表,以及更多功能。
引言
下面这套工具可用于 Web 和桌面应用程序,以简化向 .config 文件、注册表和 Windows 事件日志中读写数据的过程。虽然所提供的代码会将数据保存到系统注册表,但你可以轻松地用自己的提供程序替换 RegistryAccess
类,从而将数据保存/加载到数据库、文件或其他来源。该库还包含其他工具,用于加密和解密数据、对类中的属性进行排序以及序列化数据。注册表访问功能旨在与 MVVM 设计模式良好协作,能很好地接入“保存”和“更改”事件处理程序。这些工具应适用于大多数 Web 应用程序、WPF 和 Windows Forms 应用程序。考虑到它引用了 Windows 事件日志和系统注册表等内容,我没有尝试让它支持 Silverlight 或移动平台。该库包含以下内容
- RegSave - 用于指示类的某个属性应保存到 Windows 注册表并从中加载的属性特性 (Property Attribute)
- RegistryAccess - 简化注册表访问的工具
- Log - 简化 Windows 事件日志条目写入的工具
- CFG - 简化从 machine.config、web.config 和 app.config 文件读取的工具
- PropOrder - 用于为类的属性分配排序顺序的特性 (Attribute)
- StringEncrypter - 加密和解密字符串数据
- SerializableDictionary - 将键/值对 (
Dictionary
) 序列化为文本 - GenericComparer - 用于动态创建
IComparer
,对半动态的 LINQ 排序很有用 - Tools/ExtensionMethods - 其他工具使用的支持函数
Using the Code
RegSave
RegSave
是一个系统特性,专为 MVVM 的 ViewModel 设计,用于修饰需要保存到系统注册表和从中加载的属性。对我来说,这是这个库中最好的功能,因为它以相当优雅的方式,用很少的额外代码,平衡了数据的短期存储和长期存储。
[RegSave]
public int PropA { set; get; }
[RegSave]
public string PropB { set; get; }
要启用保存和加载功能,请将事件处理程序接入特定的 Save
和 Load
事件(例如在应用程序启动和关闭时)。
public void OnLoad(…){
// Load all RegSave tagged properties for this class from values saved in the registry
RegSave.LoadRegSaveProperties(this);
}
public void ButtonSaveClick(…){
// Save all RegSave tagged properties to the registry
RegSave.SaveRegSaveProperties(this);
}
或者,如果你的类实现了 INotifyPropertyChagned
接口,你可以将 RegSave
接入你的 OnPropertyChanged
处理程序。
this.PropertyChanged += new PropertyChangedEventHandler((o, e) =>
{
RegSave.ProperyChangeRegSave(o, e.PropertyName);
});
还有一些附加功能,可以指定某个属性是否应该被保存或加载,以及一个可选的加密开关。请参阅代码深入探讨部分或查看代码中的行内注释以获取完整详情。
RegistryAccess
此类提供了对系统注册表项“HKEY_CURRENT_USER\Software\<company>\<application>\<value>”的便捷访问。其功能包括处理子项、删除项的额外功能,并内置了错误管理和注册表句柄管理(在函数调用返回前会将更改刷新到注册表)。
RegistryAccess.Set("My Key", "Hello");
…
var The_Value = RegistryAccess.Get("My Key");
其底层类 RegistryAccessor
也可以用于将功能扩展到核心应用程序键之外。
var ra = new RegistryAccessor
("MyOtherCompany", "MyOtherProject");
ra.Set("My Other Key", "Goodbye");
Log
此类提供了向 Windows 事件日志写入的便捷方式,并内嵌了错误捕获机制,以避免在记录错误时发生错误,而这通常意味着你已经处于错误状态。实际的日志条目会在程序集链(入口、调用、执行,最后是当前程序集)中查找,以尝试获取最具描述性的消息“来源”。
Log.Write(EventLogEntryType.Information, "Hello World");
Log.Write(EventLogEntryType.Information, "Test Message: {0}", DateTime.Now);
Log.WriteError("Test Message 2: {0}", DateTime.Now);
Log.WriteError(new Exception("Test Message 3"));
CFG
此类提供了对 .config 文件的便捷访问,包括在缺少条目时的错误管理,以及环境管理,允许在通往生产环境的不同环境中拥有不同的设置。其预期用法是,每个环境的 machine.config 文件中都有一个名为“Environment”的 <appsetting>
,然后每个 web.config(或 app.config)都可以利用这个设置。例如,machine.config 可能包含这样的设置……
<appSettings>
<add key="Environment" value="DEV"/>
然后,Web.config 可以根据需要为特定于环境的设置添加前缀,或者不加前缀表示默认值。
<appSettings>
<add key="DEV_ServiceURL" value="https://dev...svc"/>
<add key="PROD_ServiceURL" value="https://prod...svc"/>
<add key="ServiceURL" value="https://default...svc"/>
获取实际值的调用代码可以在获取特定于环境的值和不获取之间切换。例如,要获取特定于环境的设置,或者在找不到环境设置时获取默认设置,代码可能如下所示
var svc_url = CFG.EnvironmentSetting("ServiceURL");
如果我们只想获取一个常规的应用设置,我们也可以这样做,并且在发生错误时(比如找不到设置),我们可以在一行代码内指明希望返回什么,使代码变得简洁明了。
var app_setting = CFG.AppSetting
("MySetting", "Value When Not Found");
由于我们期望具有一定的环境感知能力,因此还有一些用于检查环境的附加属性,例如
bool is_prod = CFG.IsProduction; // Environment that is,
// or should be treated like, a production system
bool is_local = CFG.IsLocal; // Local development environment.
PropOrder
这是另一个系统特性,用于定义类属性的顺序,在使用反射显示类详细信息或定义某种执行顺序时非常有用。
[PropOrder(10)]
public int Alpha {Set; get;}
[PropOrder(5)]
public DateTime Beta {set; get;}
…
PropertyInfo[] props = PropOrder.GetPropertiesInOrder(this);
// Props[0] would be Beta since 5 < 10
// Props[1] would be Alpha since 10 > 5
StringEncrypter
这个库大部分是从网上复制的,我相信原始来源是这个 (www.dalepreston.com),但因为是很久以前复制的,所以我不能 100% 确定。
这段代码使用 RijndaelManaged API 来加密和解密字符串。在纯客户端代码的应用程序中使用时要小心,因为任何硬编码的私钥都可能通过反编译代码被提取出来。理想情况下,这段代码应该只在服务器上使用,你可以通过安全的服务来暴露加密和解密调用。
String enc_string = StringEncrypter.StringEncrypt("Hello World", "password");
// enc_string = "22323201Z0Z4Z...5254"
String dec_string = StringEncrypter.StringDecrypt(enc_string, "password");
// dec_string = "Hello World"
SerializableDictionary
这段代码取自 weblogs.asp.net/pwelter34。
基本上,这个类只是一个可 XML 序列化的 Dictionary
(键/值对列表),因此可以轻松地存储批量数据或从服务调用中返回。
var sd = new SerializableDictionary<string, DateTime>();
sd.Add("Key1", DateTime.Now);
sd.Add("Key2", new DateTime(2000, 1, 1));
string ser = sd.Serialize();
/*
ser will then look like this: @"
<r>
<el>
<k>
<string>Key1</string>
</k>
<v>
<dateTime>2013-11-06T11:29:53.1727908-08:00</dateTime>
</v>
</el>
<el>
<k>
<string>Key2</string>
</k>
<v>
<dateTime>2000-01-01T00:00:00</dateTime>
</v>
</el>
</r>
"
*/
var de_ser = SerializableDictionary<string, DateTime>.Deserialize(ser);
var dt = de_ser["Key1"];
// dt will be the valid datetime value set above
GenericComparer
一个用于创建动态或有限用途 IComparer
的实用工具类。如果你真的需要一个用于广泛应用的 IComparer
,那么就应该实际创建一个,对吧?例如:在这种情况下,排序算法需要根据用户输入而改变,所以我们可能会这样做
GenericCompare<FileInfo> SortCompare;
if (SortMode == Mode.Name)
{
// Create a sorter based on case insensitive file names..
SortCompare = new GenericCompare<FileInfo>((fr1, fr2) =>
{
return fr1.FullName.ToLower().CompareTo(fr2.FullName.ToLower());
});
}
else if (SortMode == Mode.Size)
{
// Create another based on file size…
SortCompare = new GenericCompare<FileInfo>((fr1, fr2) =>
{
return fr1.Size.CompareTo(fr2.Size);
});
}
…
var res = (new DirectoryInfo("C:\\TestFolder"))
.GetFiles()
.OrderBy(fi => fi, SortCompare);
我知道还有其他解决这个问题的讨论,比如在一个 IComparer
的 Compare()
函数中使用 switch 语句,那也是一个很好的解决方案。这只是解决问题的另一种不同方式。
Tools/ExtensionMethods
上述库所使用的辅助工具。我非常喜欢并在很多项目中都使用的一个是 GetUnderlyingTypeIfNullable
,它接受一个 Type,如果该类型是可空的(即可空类型,如 int?
或 Nullable<int>
),它将返回对应的非可空等价类型,例如 typeof(int)
。另一个很酷的扩展方法是 PropertyInfo
上的 GetFirstCustomAttribute()
,它简化了获取单个自定义特性的过程。关于这些以及其他函数的详细信息在代码的行内注释中有解释,下面几节还会进行一些额外的探讨。
代码深入探讨
理解所有这些代码作用的最好方法是下载它并查看代码中的注释。为了帮助你入门,本节将按类名分解,重点介绍一些更有趣的部分。
CFG
这是一个我从 .NET 1.1 开始就一直在有效使用的旧库。这个库能够应对配置设置缺失的情况,并具有环境感知能力,这对于在通往生产环境的过程中进行代码推送非常有用。
public static string AppSetting(string Key,
bool EnvironmentSpecific, bool ThrowErrors, string ErrorDefault)
{
// Get the fully qualified key based on what they are seeking.
string FullKey = ((EnvironmentSpecific &&
!String.IsNullOrEmpty(Environment)) ? (Environment + "_") : "") + Key;
…
// See if the environment specific key exists
if (!ConfigurationManager.AppSettings.AllKeys.Contains(FullKey))
{
// If we can't find the environment specific key, look for a generic key
FullKey = Key;
}
// Get the value
result = (ConfigurationManager.AppSettings[FullKey] ?? "").Trim();
if (String.IsNullOrEmpty(result)
&& !ConfigurationManager.AppSettings.AllKeys.Contains(FullKey))
{
// The value was not found, set to default value
result = ErrorDefault;
…
}
// Return the value if there were no error or they don't want to see errors
if (!ThrowErrors || String.IsNullOrEmpty(ErrorMessage)) {
return result;
}
…
// Optional raise errors or return a blank string.
}
GenericCompare
坦白说,可能有更好的方法来解决即时比较器的问题,但我喜欢这个,因为它通用且可移植(依赖项少)。为了增加它的价值,null
检查可以保证传入 compare
函数的两个值都不会是 null
,这使得编写比较函数变得更容易。
// Execute the compare
public int Compare(T x, T y)
{
if (x == null || y == null)
{
// These 3 are bell and whistles to handle cases where
// one of the two is null, to sort to top or bottom respectively
// the advantage being that the compare function
// is guaranteed to have not null values passed to it.
if (y == null && x == null) { return 0; }
if (y == null) { return 1; }
if (x == null) { return -1; }
}
try
{
// Do the actual compare
return ComparerFunction(x, y);
}
catch (Exception ex) {…}
}
该函数也必须在构造时设置,我这样做更多是出于语法原因(代码看起来更整洁),而不是其他原因。
private Func<T, T, int> ComparerFunction { set; get; }
// Constructor
public GenericCompare(Func<T, T, int> comparerFunction)
{
ComparerFunction = comparerFunction;
}
PropOrder
如果你熟悉反射,那么 PropOrder
并没有什么神奇之处。关键在于它继承自 System.Attribute
,然后提供了一种有意义的方式来获取该特性,并且更重要的是,处理了它不存在的情况。请注意,这个函数的很多代码都在共享的扩展库中,稍后会解释。
public static int? GetOrder(PropertyInfo p, int? errorDefeault)
{
if (p == null)
{
return errorDefeault;
}
// GetFirstCustomAttribute custom extension explained later
var pop = p.GetFirstCustomAttribute <PropOrder>(true);
// If the property does not contain the custom attribute, return the default value
if (pop == null)
{
return errorDefeault;
}
return pop.Order;
}
注册表访问 (Registry Access)
在它的大部分生命周期中,这是一个静态 (static)
类,但为了这次发布,我将其转换为在底层使用单例模式以扩展灵活性。这个库帮助解决的一个关键问题是,给定应用程序的所有注册表读/写操作都植根于同一个地方,由硬编码的根 HKCU\Software 以及强制的公司和应用名称来构成树中的下两级节点。
// Constructor
public RegistryAccessor(string CompanyRoot, string AppName)
{
if (String.IsNullOrEmpty(CompanyRoot))
{
throw new ArgumentException
("COMPANY_ROOT cannot be blank", "CompanyRoot");
}
if (String.IsNullOrEmpty(AppName))
{
throw new ArgumentException
("APP_NAME cannot be blank", "AppName");
}
// Root under HKCU\Software\<CompanyRoot>\<AppName>
COMPANY_ROOT = Path.Combine(@"SOFTWARE\", CompanyRoot);
APP_NAME = AppName;
}
另一个关键的好处是提交模式,即每次访问注册表要么被包裹在 using()
块中,要么在操作完成后调用 Close()
,这样更改在退出函数前就会被提交。对于需要大量写入注册表的应用程序,这可能会带来性能问题,敬请注意。无论好坏,这个库基本上只设计用于处理字符串 (String)
数据,这对于我使用它的应用程序来说,让我的工作变得更轻松。
private String GetFrom(string sKey, string sFullPath, string ErrorDefault)
{
try
{
// Using to make sure registry is properly closed when we are done with our read
using (RegistryKey Reg = Registry.CurrentUser.OpenSubKey(sFullPath))
{
// If the key is missing, handle it gracefully
if (Reg != null)
{
// For better or worse, always return strings
return String.Format("{0}", Reg.GetValue(sKey));
}
}
}
catch (Exception ex1){…}
return ErrorDefault;
}
最后,这里有相当不错的异常处理机制,试图优雅地处理当你尝试获取某个东西但键不存在的情况。
RegSave
RegSave 中有几个我喜欢的功能,这也是我发布这篇文章的全部原因。我喜欢使用 Attribute
标签来解决问题,因为它的侵入性低且简单,在不同模型下使用相对简单,以及这个通用概念如果需要可以扩展到其他存储库。例如,你可以使用相同的方法将数据写入本地 cookie 文件,而不是在桌面或 Silverlight 应用程序中写入注册表。下面是一些值得注意的代码片段,第一个是从给定属性中获取特性详情的函数,或者在未找到时获取默认值。
public static RegSave GetRegSaveAccess(PropertyInfo pi)
{
// Negative return option, note that this is
// where we set the CurrentProperty for later use
var ret_atrb = new RegSave
{
Load = false,
Save = false,
Encrypt = false,
CurrentProperty = pi
};
// Arbitrarily grab the first one
var found_atrb = pi.GetFirstCustomAttribute(ret_atrb, true);
// And set return values…
ret_atrb.Load = found_atrb.Load;
ret_atrb.Save = found_atrb.Save;
ret_atrb.Encrypt = found_atrb.Encrypt;
return ret_atrb;
}
然后,当我们试图找到所有 RegSave 属性时,我们依赖那个函数来获取 CurrentProperty
,这样我们就可以确定访问权限。实际的访问权限是他们在 RegSave
特性中指定的权限与属性本身支持的权限(r
和 w
变量)合并而成的。
/// <summary>
/// Aggregated access based on a comparison of the CurrentProperty and Load/Save values
/// </summary>
public ReadWriteFlag CurrentAccess
{
get
{
if (CurrentProperty == null)
{
return ReadWriteFlag.Neither;
}
// Compare the tags desire with what the object will support
bool r = CurrentProperty.CanRead && Save;
bool w = CurrentProperty.CanWrite && Load;
// And return the proper setting
if (r && w)
{
return ReadWriteFlag.Both;
}
else if (w)
{
return ReadWriteFlag.Write;
}
else if (r)
{
return ReadWriteFlag.Read;
}
return ReadWriteFlag.Neither;
}
}
所有这些在获取特定类型的所有属性时结合在一起。
public static RegSave[] GetRegSaveProps
(Type ty, bool readable, bool writable, bool includeEncrypted)
{
if (ty == null) { return new RegSave[] { }; }
return ty.GetProperties()
.Select(p => GetRegSaveAccess(p))
.Where(rs =>
{
// Exclude encrypted items when instructed to do so
if (rs.Encrypt && !includeEncrypted)
{
return false;
}
var ca = rs.CurrentAccess;
// If not readable, but requested returned values must be readable, exclude item
if (readable && !(ca == ReadWriteFlag.Read || ca == ReadWriteFlag.Both))
{
return false;
}
// If not writable, but requested returned values must be writable, exclude item
if (writable && !(ca == ReadWriteFlag.Write || ca == ReadWriteFlag.Both))
{
return false;
}
return true;
})
.ToArray();
}
所有这些都与上面重点介绍的 Save
/Load
方法一起工作,可以将特定的保存和加载事件处理程序接入代码中,或者你可以使用现成的 OnPropertyChagned
方法,但无论哪种情况,最终你都会将属性保存到注册表。
public static void SaveRegSaveProperties
(object o, string subPath, string[] propertyNames)
{
bool AnyProp = (propertyNames == null || propertyNames.Count() == 0);
// Find matching RegSave properties
var matchProps = GetRegSaveProps(o.GetType(), true, false, true)
.Where(itm => AnyProp || propertyNames.Contains(itm.CurrentProperty.Name));
// Get the matching property where we can read/write and matches name
foreach (var rsa in matchProps)
{
// Get the changed value as a string
string strVal = String.Format("{0}", rsa.CurrentProperty.GetValue(o, null));
// If the value should be encrypted before writing, do so here.
if (rsa.Encrypt)
{
strVal = StringEncrypter.StringEncryptWithCheck
(strVal, rsa.CurrentProperty.Name);
}
// Finally, save the value to the registry.
RegistryAccess.Set(rsa.CurrentProperty.Name,
strVal, RegistryAccessor.EscapeSubPath(subPath));
}
}
相应的加载操作(从注册表中保存的值设置类属性)
public static void LoadRegSaveProperties
(object o, string subPath, string[] propertyNames)
{
bool AnyProp = (propertyNames == null || propertyNames.Count() == 0);
// Find matching properties
var matchProps = GetRegSaveProps(o.GetType(), false, true, true)
.Where(itm => AnyProp || propertyNames.Contains(itm.CurrentProperty.Name));
foreach (var rsa in matchProps)
{
// Get the value as a string
string strVal = String.Format("{0}",
RegistryAccess.Get(rsa.CurrentProperty.Name, SubPath, String.Empty));
if (rsa.Encrypt)
{
// If we need to decrypt it, do so here.
strVal = StringEncrypter.StringDecryptWithCheck(strVal, rsa.CurrentProperty.Name);
}
// Set the property on the object, of proper type
if (!String.IsNullOrEmpty(strVal))
{
rsa.CurrentProperty.SetValue(o, Tools.StringToObject(
rsa.CurrentProperty.PropertyType, strVal), null);
}
}
}
关于 RegSave
中 subPath 的一个小说明……由于原始代码设计用于处理每个应用程序都有唯一名称的 ViewModel,所以总是绑定到类型进行保存是可以的。但是,如果你有一个类用于存储通用数据,你很可能需要传入一个子路径来区分多次保存和加载。例如
public class Person {
[RegSave]
public string Name {set;get;}
[RegSave]
public int Age { set; get; }
}
var person_a = new Person {Name = "Joe", Age = 10};
var person_b = new Person {Name = "Mary", Age = 10};
RegSave.SaveRegSaveProperties(person_a, person_a.Name);
RegSave.SaveRegSaveProperties(person_b, person_b.Name);
通过将 .Name
属性传递给 SaveRegSaveProperties()
的第二个参数,每个人的数据将存储在他们自己的注册表项中("HKCU\Software\MyCompany\MyApp\Joe\Person" 和 "HKCU\Software\MyCompany\MyApp\Mary\Person"),否则他们都会读写到 "HKCU\Software\MyCompany\MyApp\Person",从而覆盖彼此的数据。
StringEncrypter
如上所述,这个库大部分是从网上的一篇文章复制的。不过,我增加了一个处理空密码的适配,通过“KeyPrefix
”的方式。
在使用这部分代码之前,请阅读代码中的所有注释,特别是如果你正在开发客户端应用程序,因为反编译你的代码可能会暴露私钥。
工具 / 扩展方法 (Tools / ExtensionMethods)
这两个类包含各种杂项工具,这些工具在多个项目中被使用(或在此项目之外提供用途)。其中一个很有帮助的是 StringToObject
,它扩展了基本的 Convert.ChangeType()
以支持更多不同的类型。
public static object StringToObject(Type outputType, string value)
{
// Garbage in, garbage out.
if (outputType == null)
{
return null;
}
// If the input type was nullable, try to get the non-nullable value first
var ty = GetUnderlyingTypeIfNullable(outputType);
bool is_null = String.IsNullOrEmpty(value);
// If it's an enum value, try to cast the value to an enum of proper type
if (ty.IsEnum)
{
if (is_null)
{
return Enum.ToObject(outputType, 0);
}
else
{
return Enum.Parse(ty, value);
}
}
else
{
// Else do a standard type convert
if (is_null)
{
if (outputType == typeof(string))
{
return String.Empty;
}
else
{
return null;
}
}
return Convert.ChangeType(value, ty);
}
}
另一段值得注意的代码是 ProperyInfo
的扩展方法 GetFirstCustomAttribute
。这解决了我之前使用 GetCustomAttributes()
时遇到的一些问题,处理了属性没有自定义特性或具有太多相同类型特性的问题,并通过在失败时(如未找到特性)返回一个已知值来帮助进行错误管理。
public static T GetFirstCustomAttribute<t>
(this PropertyInfo pi, T errorDefault, bool inherit)
where T : class
{
if (pi == null) { return errorDefault; }
// Get all custom attributes
var atrbs = pi.GetCustomAttributes(typeof(T), inherit);
// If none are found, return errorDefault
if (atrbs == null || atrbs.Length == 0)
{
return errorDefault;
}
// Arbitrarily grab the first one found, ensuring type is set (as T)
var ret = atrbs[0] as T;
// Ensure the cast did work as expected (paranoia)
if (ret == null)
{
return errorDefault;
}
return ret;
}
历史
- 2013年11月6日:首次公开发布。
- 2013年11月8日:添加了演示应用。
- 2013年11月18日:对
GenericCompare
进行了小幅更新,以支持比较两种不同的类型。 - 2013年11月23日:修正了文章中的拼写错误,误将 CFG 称为“CGF”。代码未变。
- 2014年5月5日:
SerializableDictionary
中的 Bug 修复。如果一个类实现了 2 个SerializableDictionary
属性,在反序列化时,第二个属性无法正确反序列化。这个失败(和修复)的证据在单元测试SerializableDictionary_MultiplePropTest()
中。修复位于SerializableDictionary.ReadXml()
,该方法中缺少了最后的.Read()
。对于给您带来的不便,我深表歉意。