ResEditor 的用处前提
1, MVC
2, 需要设置字段的显示名称 Display
3, 用资源文件
背景:
我们的项目是 MVC5 + ORACLE + EF Db First
需求分析师兼任数据库设计, 目前有141张表, 70% 的表,字段数在100个以上.
加 Display 特性一般由两个途径:
1,直接在实体类上添加
2,用伴随类.
但是实体类是由 EF 的TT模板自动生成的,虽然可以修改 TT 文件加上 Display 特性到属性上,但是字段的描述不适合直接拿来当Display
如果用伴随类, 需要新增 100 多个伴随类, 几千个属性, 完全手输,不实现.
下面这部份是炒旧菜, 早在2年前博文: MVC3 项目总结 中已经介绍过.
这里只是把它做了一点修改, 便于机械式的批量操作.
1 public class ResDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider { 2 3 private ResourceManager ResMgr; 4 5 public ResDataAnnotationsModelMetadataProvider(ResourceManager resMgr) { 6 7 if (resMgr == null) 8 throw new ArgumentNullException("resMgr"); 9 10 this.ResMgr = resMgr; 11 } 12 13 private string GetString(Type containerType, string propertyName) { 14 var key = string.Format("{0}{1}_{2}_DisplayName", containerType.Namespace.Replace(".", ""), containerType.Name, propertyName); 15 return this.ResMgr.GetString(key); 16 } 17 18 /// <summary> 19 /// 重写生成元数据的方法 20 /// 只有页面上来通过Lambda表达式过来的元数据,才进行中英文转换 21 /// (该过滤为了减少元数据的中英文转换次数) 22 /// </summary> 23 /// <param name="attributes"></param> 24 /// <param name="containerType"></param> 25 /// <param name="modelAccessor"></param> 26 /// <param name="modelType"></param> 27 /// <param name="propertyName"></param> 28 /// <returns></returns> 29 protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { 30 if (containerType != null) {//请改之前先做好测试!这是最终的条件. 31 32 //动态类型/代理类 EF 33 var t = containerType.Assembly.IsDynamic ? containerType.BaseType : containerType; 34 35 var v = this.GetString(t, propertyName); 36 if (!string.IsNullOrWhiteSpace(v)) { 37 //这里,如果 v 是空字符串的话,居然会影响到 SmartModelBinder 的 ModelValidator.GetModelValidator 38 var dsp = new DisplayAttribute() { 39 Name = v 40 }; 41 var attrs = attributes.ToList(); 42 attrs.RemoveAll(a => a is DisplayAttribute); 43 attrs.Add(dsp); 44 return base.CreateMetadata(attrs, containerType, modelAccessor, modelType, propertyName); 45 } 46 } 47 48 return base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName); 49 } 50 }
如上代码所示, 要求资源文件中的 Key 按一下格式设置:
{0}{1}_{2}_DisplayName
var key = string.Format("{0}{1}_{2}_DisplayName", containerType.Namespace.Replace(".", ""), containerType.Name, propertyName);
这个要求很高, 不适合手工直接在VS中编辑.
怎么办呢? 于是我就写了这个 ResEditor
当选定一个 ResX 文件后, 程序会自动尝试加载资源文件的多语言文件.
比如选择了 EntityRes.Resx , 会把同目录下的 EntityRes.en-US.resx / EntityRes.zh-TW.resx 等文件给找出来:
1 private Dictionary<string, string> GetLangFiles(string path) { 2 var name = this.GetResFileName(path); 3 var dir = Path.GetDirectoryName(path); 4 var files = Directory.GetFiles(dir, string.Format("{0}.*.resx", name)); 5 6 var dic = new Dictionary<string, string>() { 7 {"Default", Path.Combine(dir, string.Format("{0}.resx", name)) } 8 }; 9 10 foreach (var f in files) { 11 var lang = Regex.Match(f, @".(?<lang>[^. ]*?).resx").Groups["lang"].Value; 12 dic.Add(lang, f); 13 } 14 15 return dic; 16 }
然后跟据找到的语言, 动态生成一个实体类:
1 private void DefineType(IEnumerable<string> langs) { 2 var tb = TypeBuilderHelper.Define("TTT", typeof(Record)); 3 langs.ToList().ForEach(l => { 4 tb.DefineProperty(l.Replace("-", "_"), typeof(string)); 5 }); 6 this.TmpType = tb.CreateType(); 7 }
TypeBuilderHelper 是直接拿
http://www.codeproject.com/Articles/206416/Use-dynamic-type-in-Entity-Framework-SqlQuery
中的代码,做了少许修改:
1 public static class TypeBuilderHelper { 2 3 public static TypeBuilder Define(string typeName, Type parentType = null) { 4 var typeBuilder = AppDomain.CurrentDomain 5 .DefineDynamicAssembly(new AssemblyName("Test"), AssemblyBuilderAccess.Run) 6 .DefineDynamicModule("Test") 7 .DefineType("DT", TypeAttributes.Public); 8 typeBuilder.DefineDefaultConstructor(MethodAttributes.Public); 9 10 if (parentType != null) 11 typeBuilder.SetParent(parentType); 12 13 return typeBuilder; 14 } 15 16 public static void DefineProperty(this TypeBuilder builder, string propertyName, Type propertyType) { 17 const string PrivateFieldPrefix = "m_"; 18 const string GetterPrefix = "get_"; 19 const string SetterPrefix = "set_"; 20 21 // Generate the field. 22 FieldBuilder fieldBuilder = builder.DefineField( 23 string.Concat(PrivateFieldPrefix, propertyName), 24 propertyType, FieldAttributes.Private); 25 26 // Generate the property 27 PropertyBuilder propertyBuilder = builder.DefineProperty( 28 propertyName, PropertyAttributes.HasDefault, propertyType, null); 29 30 // Property getter and setter attributes. 31 MethodAttributes propertyMethodAttributes = 32 MethodAttributes.Public | MethodAttributes.SpecialName | 33 MethodAttributes.HideBySig; 34 35 // Define the getter method. 36 MethodBuilder getterMethod = builder.DefineMethod( 37 string.Concat(GetterPrefix, propertyName), 38 propertyMethodAttributes, propertyType, Type.EmptyTypes); 39 40 // Emit the IL code. 41 // ldarg.0 42 // ldfld,_field 43 // ret 44 ILGenerator getterILCode = getterMethod.GetILGenerator(); 45 getterILCode.Emit(OpCodes.Ldarg_0); 46 getterILCode.Emit(OpCodes.Ldfld, fieldBuilder); 47 getterILCode.Emit(OpCodes.Ret); 48 49 // Define the setter method. 50 MethodBuilder setterMethod = builder.DefineMethod( 51 string.Concat(SetterPrefix, propertyName), 52 propertyMethodAttributes, null, new Type[] { propertyType }); 53 54 // Emit the IL code. 55 // ldarg.0 56 // ldarg.1 57 // stfld,_field 58 // ret 59 ILGenerator setterILCode = setterMethod.GetILGenerator(); 60 setterILCode.Emit(OpCodes.Ldarg_0); 61 setterILCode.Emit(OpCodes.Ldarg_1); 62 setterILCode.Emit(OpCodes.Stfld, fieldBuilder); 63 setterILCode.Emit(OpCodes.Ret); 64 65 propertyBuilder.SetGetMethod(getterMethod); 66 propertyBuilder.SetSetMethod(setterMethod); 67 } 68 69 }
生成动态类型后, 在把资源文件通过 ResXResourceReader 读出来, 生成一个动态类型的数据集合, 作为数据源, 展示到界面上.
1 private void ReadResx(Dictionary<string, string> dic) { 2 3 List<dynamic> results = new List<dynamic>(); 4 5 foreach (var d in dic) { 6 try { 7 8 using (var reader = new ResXResourceReader(d.Value)) { 9 foreach (DictionaryEntry entry in reader) { 10 try { 11 12 if (entry.Key == null || entry.Value == null || entry.Key.GetType() != typeof(string) || entry.Value.GetType() != typeof(string)) 13 continue; 14 15 var key = (string)entry.Key; 16 17 var o = results.FirstOrDefault(r => r.Key.Equals(key)); 18 if (o == null) { 19 o = Activator.CreateInstance(this.TmpType); 20 o.Key = (string)entry.Key; 21 o.IsExists = true; 22 results.Add(o); 23 } 24 25 this.TmpType.GetProperty(d.Key.Replace("-", "_")) 26 .SetValue(o, (string)entry.Value); 27 } catch { 28 } 29 } 30 } 31 } catch (Exception ex) { 32 MessageBox.Show(ex.Message); 33 } 34 } 35 36 if (results.Count == 0) 37 results.Add(Activator.CreateInstance(this.TmpType)); 38 39 App.Current.Dispatcher.Invoke(() => { 40 this.Datas = new BindableCollection<dynamic>(results); 41 this.NotifyOfPropertyChange(() => this.Datas); 42 43 this.CV = CollectionViewSource.GetDefaultView(this.Datas); 44 this.CV.Filter = new Predicate<object>(this.Filter); 45 this.CV.GroupDescriptions.Add(new PropertyGroupDescription("Key") { 46 Converter = new Converter1() 47 }); 48 this.CV.SortDescriptions.Add(new SortDescription("Key", ListSortDirection.Ascending)); 49 this.NotifyOfPropertyChange(() => this.CV); 50 }, DispatcherPriority.Send); 51 }
保存的时候, 通过 ResXResourceWriter 将数据集合中的内容写回 Resx 文件.
上面这些全都是为了多语言做准备. 下面说说我的多语言处理方式
A, 自定义一个 MvcRouteHandler , 用来处理多语言:
public class MultiLangRouteHandler : MvcRouteHandler { protected override System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext) { string lang = requestContext.RouteData.Values["lang"].ToString(); try { var culture = CultureInfo.GetCultureInfo(lang); Thread.CurrentThread.CurrentUICulture = culture; } catch { } return base.GetHttpHandler(requestContext); } }
它的作用就是拿来获取路由的 lang 值, 然后设置 CurrentUICulture.
B, 在路由配置中, 把默认的路由配置修改成:
1 public class RouteConfig { 2 public static void RegisterRoutes(RouteCollection routes) { 3 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 4 5 //routes.MapRoute( 6 // name: "Default", 7 // url: "{controller}/{action}/{id}", 8 // defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 9 //); 10 11 routes.Add(new Route("{lang}/{controller}/{action}/{id}", 12 new RouteValueDictionary(new { 13 lang = "zh-CN", 14 controller = "Home", 15 action = "Index", 16 id = UrlParameter.Optional 17 }), new MultiLangRouteHandler())); 18 } 19 }
这样做之后, 一个简单的语言切换就完成了, 但是...
假如有如下实体:
public class Person { public int ID { get; set; } [Display(Name = "姓名1")] [DisplayName("姓名2")] public string Name { get; set; } public string EnName { get; set; } public DateTime? Birthday { get; set; } public string Address { get; set; } public string Email { get; set; } }
当语言为 英语的时候, 显示 EnName 的值, 为简体中文/繁体中文的时候, 显示 Name 的值, 怎么办呢? if..else.. ? 我猜你也会这样想.
现在不用了, 从MVC 4 开始, 加入了 DisplayModel, 这里我用它来变个花样, 在 Global 中添加:
1 DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("en") { 2 ContextCondition = ctx => { 3 var data = RouteTable.Routes.GetRouteData(ctx); 4 var lang = (string)data.Values["lang"]; 5 return string.Equals(lang, "en-US", StringComparison.OrdinalIgnoreCase); 6 } 7 }); 8 9 DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("tw") { 10 ContextCondition = ctx => { 11 var data = RouteTable.Routes.GetRouteData(ctx); 12 var lang = (string)data.Values["lang"]; 13 return string.Equals(lang, "zh-tw", StringComparison.OrdinalIgnoreCase); 14 } 15 });
即当 lang 为 en-US 的时候, 使用 DisplayModel en, tw 同理.
接着把 cshtml 文件复制一份出来, 我这里以 index.cshtml 为例, 复制的文件改名为 index.en.cshtml.
1 <table border="1" cellpadding="5"> 2 @foreach (var p in Model) { 3 <tr> 4 <td>@p.Name</td> 5 <td>@p.Address </td> 6 <td>@p.Email</td> 7 <td>@Html.ActionLink( UIRes.Edit , "Edit", new { id = p.ID })</td> 8 </tr> 9 } 10 </table>
复制出来的文件改成:
1 <table border="1" cellpadding="5"> 2 @foreach (var p in Model) { 3 <tr> 4 <td>@p.EnName</td> 5 <td>@p.Address </td> 6 <td>@p.Email</td> 7 <td>@Html.ActionLink("Edit", "Edit", new { id = p.ID })</td> 8 </tr> 9 } 10 </table>
除了把 Name 列换成 EnName 之外,两个文件的内容基本一样.
这样, 当匹配到 lang 为 en-US 的时候,就会用 index.en.cshtml , 而不是 index.cshtml
如果不存在 index.en.cshtml 也不要紧, 会取 index.cshtml 的.
ResEditor 源码:
http://files.cnblogs.com/xling/ResEditor.7z
多语言示例源码:
http://files.cnblogs.com/xling/MutLangTest.7z
下一篇:
EF5 Db First + ORACLE 疑难杂症