• 代码笔记:使用Xml2Linq和CodeDom自动重整多语化资源文件


    本贴没啥干货,纯碎只是记录下写过的代码,路过的大侠们可以绕道走~

    背景:项目的多语化的Message使用的是:

    1. 用XML Messages.xml 来存放languages resource
    2. 使用C# MessageKeys 类存放所有Message key及其在XML资源文件中的对应Message ID
    3. 在代码中调用这个MessageKey

    现在,项目有个Refactor工作,他们设计了更高一级别的Common Message,这样就不希望各个子项目中的message太繁杂难管理,而我“不幸”被分派了这个比较坑妈的活,就是将我当时所在项目的近2000条Message的一条一条的整理查看,并将可以转换成Common Message的转换掉,而且Message ID和Message Code都要重新整理,且里面的描述性文字都得由BA重新审核了改掉。这样就是个很痛苦的手工活。我想想就受不了,这眼得瞎,只得想了个办法。

    1. 将Messages.xml的文件及对应的MessageKey name导出成excel,这样大家可以看着分析excel
    2. 经过大家的讨论消减后,得到最终化的excel
    3. 将excel重新导成Messages.xml,以及C# MessageKeys类(因为之前有将MessageKey也导出到EXCEL中,是为了能导出)
    4. 各程序员们开始进行代码重构,然后BA可以再慢慢对DESCRIPTION修修补补,我就可以再使用这个自动程序生成XML
    5. 将来有任何变化,可以直接改了XML,再自动生成CLASS,很方便

    于是,这里面就要用到几个小技术

    1. 为XML构建一些DTO (Data Transfer Object)
    2. 导出XML为excel,这个要用到System.Xml.Linq
    3. 导出excel为XML,这个要使用到System.Xml.Serialization,以及使用Excel作为数据源(直接用的connection string,没有用interrop)
    4. 导出excel为C#代码文件,使用System.CodeDom

    多的不说了,上代码了,第一次使用CodeDom自动生成C#,还是挺有意思的。

    XML Messages.xml 文件

    <Messages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 
      <MessagesByType typeCode="1" typeName="Validation Error"> 
        <Message id="110"> 
          <EventId>50</EventId> 
          <Code>110</Code> 
          <ShortText>Invalid value </ShortText> 
          <Description>{0} can't be less than {1}.</Description> 
          <Variables> 
            <Variable formatIndex="0">Field name</Variable> 
            <Variable formatIndex="1">Field name</Variable> 
          </Variables> 
        </Message> 
      </MessagesByType> 
      <MessagesByType typeCode="2" typeName="Application Error"> 
        <Message id="410"> 
          <EventId>50</EventId> 
          <Code>410</Code> 
          <ShortText>Invalid value </ShortText> 
          <Description>{0} can't be less than {1}.</Description> 
          <Variables> 
            <Variable formatIndex="0">Field name</Variable> 
            <Variable formatIndex="1">Field name</Variable> 
          </Variables> 
        </Message> 
      </MessagesByType> 
    </Messages>

    从上述XML来分析,进而生成以下Data Models,新建一个类名为DataObjXML

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Xml.Serialization;
    
    namespace RefactorMessages
    {
        [Serializable]
        public class Messages
        {
            // if not setting to "MessagesByType", a parent node of <MessagesByTypes> 
            // beyonds <MessagesByType> will be generated
            [XmlElement("MessagesByType")]
            public List<MessagesByType> MessagesByTypes { get; set; }
        }
    
        [Serializable]
        public class MessagesByType
        {
            [XmlElement("Message")]
            public List<Message> Messages { get; set; }
    
            [XmlAttribute]
            public string typeCode { get; set; }
            [XmlAttribute]
            public string typeName { get; set; }
        }
    
        [Serializable]
        public class Variable
        {
            [XmlAttribute]
            public string formatIndex { get; set; }
            [XmlText]
            public string text { get; set; }
        }
    
        [Serializable]
        public class Message
        {
            [XmlIgnore]
            [XmlAttribute]
            public string category { get; set; }
            [XmlAttribute]
            public string id { get; set; }
            [XmlIgnore]
            [XmlAttribute]
            public string old_id { get; set; }
            [XmlIgnore]
            public string TypeName { get; set; }
            [XmlIgnore]
            public string TypeCode { get; set; }
            [XmlIgnore]
            public string ClassName { get; set; }
            public string EventId { get; set; }
            public string Code { get; set; }
            public string ShortText { get; set; }
            public string Description { get; set; }
            public List<Variable> Variables { get; set; }
        }
    }

    在做这个XML数据实体时,要留意到的是,

    1. 之前集合属性MessagesByTypes在没有加[XmlElement("MessagesByType")]之前,反序列化后生成的XML会是<MessagesByTypes><MessagesByType>…</MessagesByType></MessagesByTypes>,这个与我想要的有出入,所以想了这个方法,试了下,没想到能成,现在就能直接成生<Messages..><MessagesByType>..</MessagesByType></Messages>,少了代表List的上层结点。
    2. 如果不希望最后序列化后出现在XML,就需要增加[XmlIgnore]属性
    3. 如果这一个属性需要序列化为属性而非元素,需要增加[XmlAttribute]以作标识
    4. 另外地球人都知道的,所有的类都需要标识为可序列化,使用[Serializable]

    虽然基本都是一些小常识,在这做个记录。

    有了这个基本数据实体后,就可以开始工作了。

    首先,需要将XML导出成我想要的可供大家坐在一起容易分析讨论的。这里面用到了System.xml.Linq,因为Message的typeCode一共可能出现5种,我想先按typeCode的id再按message的id排序再导出,完整代码如下,要注意的是,最后每个值都最好做个空值判断,(其中KeyNameInClass 就是我之后导出为C#类时需要用到的)

    private DataTable GenerateDataTableFromXml(bool includeDetect = true)
    {
        DataTable dt = null;
    
        Encoding ec = new UTF8Encoding(true);
        using (StreamReader sr = new StreamReader(_xmlPath, ec))
        {
            XDocument document = XDocument.Load(sr);
    
            var query = from msg in document.Descendants(@"MessagesByType").Descendants(@"Message")
                        select new
                        {
                            TypeCode = msg.Parent.Attribute("typeCode").Value,
                            TypeName = msg.Parent.Attribute("typeName").Value,
                            MessageId = msg.Attribute("id").Value,
                            EventID = msg.Element("EventID") == null ? string.Empty : msg.Element("EventID").Value,
                            Code = msg.Element("Code") == null ? string.Empty : msg.Element("Code").Value,
                            ShortText = msg.Element("ShortText") == null ? string.Empty : msg.Element("ShortText").Value,
                            Description = msg.Element("Description") == null ? string.Empty : msg.Element("Description").Value,
                            Variables = GetMessageVariables(msg.Element("Variables"), msg.Elements("Variable").ToList()),
                            //the xml key's field name in MessageKey class
                            KeyNameInClass = string.Join("\r\n", _allXMLKeys.Where(o => o.MessageId == msg.Attribute("id").Value).Select(o => o.ClassName).ToList())
                        };
    
            var list = query.ToList();
    
            dt = new DataTable();
            dt.Columns.Add("Type");
            dt.Columns.Add("MessageId");
            dt.Columns.Add("EventID");
            dt.Columns.Add("Code");
            dt.Columns.Add("ShortText");
            dt.Columns.Add("Description");
            dt.Columns.Add("Variables");
            dt.Columns.Add("KeyNameInClass");
    
            foreach (var o in list)
            {
                DataRow dr = dt.NewRow();
                dr["Type"] = o.TypeName;
                dr["MessageId"] = o.MessageId;
                dr["EventID"] = o.EventID;
                dr["Code"] = o.Code;
                dr["ShortText"] = o.ShortText;
                dr["Description"] = o.Description;
                dr["Variables"] = o.Variables;
                dr["KeyNameInClass"] = o.KeyNameInClass;
    
                dt.Rows.Add(dr);
            }
        }
    
        return dt;
    }

    上面的类是为了构建一个需要导出的DataTable,然后导出为csv。

    接下来,经过大家的热烈又想死的跑了近2000行的讨论分析、削削减减后,我们得到了一个全新的spreadsheet,并且将最终大家认可的保存为一个名为,let’s say,Finalized的sheet,我现在就需要将这个sheet重新导出成和之前Messages.xml一模一样格式的xml。使用到一般的ado.net,连接串技术,"Provider=Microsoft.Jet.OLEDB.4.0; data source={0}; Extended Properties=Excel 8.0;" 来取得excel数据,并得到之前定义的需要序列化为xml的Messages对象。

    private Messages PrepareObjectFromExcel(string excelFilePath, string sheetName)
    {
        var fileName = excelFilePath;
        var connectionString = string.Format("Provider=Microsoft.Jet.OLEDB.4.0; data source={0}; Extended Properties=Excel 8.0;", fileName);
    
        var adapter = new OleDbDataAdapter("SELECT * FROM [" + sheetName + "$]", connectionString);
        var ds = new DataSet();
    
        adapter.Fill(ds, sheetName);
        var data = ds.Tables[sheetName].AsEnumerable();
    
        var query = data.Select(x =>
            new Message
            {
                TypeName = x.Field<string>("Type Name"),
                TypeCode = x.Field<string>("Type Code"),
                ClassName = x.Field<string>("Key Name In Class"),
                old_id = x.Field<string>("OldMessageId"),
                category = x.Field<string>("Category"),
                id = x.Field<string>("MessageId"),
                EventId = x.Field<string>("EventID"),
                Code = GetCodeFromMessageId(x.Field<string>("MessageId")),
                ShortText = x.Field<string>("ShortText"),
                Description = x.Field<string>("Description"),
                Variables = ReadVariables(x.Field<string>("Variables")),
            }).ToList<Message>().GroupBy(o => new { o.TypeCode, o.TypeName });
    
        Messages msg = new Messages();
        var msgsByTypes = new List<MessagesByType>();
    
        foreach (var type in query)
        {
            MessagesByType msgBody = new MessagesByType
            {
                typeCode = type.Key.TypeCode,
                typeName = type.Key.TypeName,
                Messages = type.ToList()
            };
    
            msgsByTypes.Add(msgBody);
        }
    
        msg.MessagesByTypes = msgsByTypes;
    }

    然后写几行代码序列化一下保存为xml文件即可(以下为完整Serializer.cs,或见源码下载),对了,如果你想得到size更小的xml而不关心formatting的话,settings.Indent = true 是可以置为false的,

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.IO;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.IO.Compression;
    using System.Xml.Serialization;
    
    namespace RefactorMessages
    {
        /// <summary>
        /// from TFS\FOTFP\R2\Common\CBP\HP.FOT.CBP.Shared\Helpers\Serializer.cs
        /// </summary>
        public class Serializer
        {
            const int STRING_MAX_LENGTH = 16 * 1024 + 256;//the length of any string object < 16K
    
            #region for binaryformatter serialize
            public byte[] SerializeObject(object serializableObject)
            {
                MemoryStream stream = new MemoryStream();
                BinaryFormatter b = new BinaryFormatter();
                b.Serialize(stream, serializableObject);
                byte[] bytes = stream.GetBuffer();
                stream.Close();
                return bytes;
    
            }
    
            /// <summary>
            /// 
            /// </summary>
            /// <typeparam name="T">the type of the object need to be serialized</typeparam>
            /// <param name="serializableObject"></param>
            /// <returns></returns>
            public static string SerializeObjectToString<T>(T serializableObject)
            {
                Serializer serializer = new Serializer();
                byte[] bytes = serializer.SerializeObject(serializableObject);
    
                // SEP this was causing problems with deserialization so I commented it out
                //bytes = Compress(bytes);
    
                return ToString(bytes);
            }
    
            #endregion
    
            #region for binaryformatter deserialize
            public object DeSerializeObject(byte[] someBytes)
            {
                MemoryStream stream = new MemoryStream(someBytes);
                BinaryFormatter b = new BinaryFormatter();
                object retObj = b.Deserialize(stream);
                stream.Close();
                return retObj;
            }
    
            /// <summary>
            /// 
            /// </summary>
            /// <typeparam name="T">the type of the object got by deserialized</typeparam>
            /// <param name="serializedObjectBytesStr"></param>
            /// <returns></returns>
            public static T DeSerializeObject<T>(string serializedObjectBytesStr)
            {
                T retObj;
    
                try
                {
                    byte[] bytes = ToBytes(serializedObjectBytesStr);
    
                    // SEP this was causing problems with deserialization so I commented it out
                    //bytes = Decompress(bytes);
    
                    Serializer serializer = new Serializer();
                    retObj = (T)serializer.DeSerializeObject(bytes);
                }
                catch
                {
                    retObj = default(T);
                }
    
                return retObj;
            }
    
            #endregion
    
            public static string SerializeObject2XML(object serializableObject)
            {
                if (serializableObject == null) return null;
    
                return SerializeObject2XML(serializableObject, serializableObject.GetType());
            }
    
            public static string SerializeObject2XML(object serializableObject, Type type)
            {
                if (serializableObject == null) return null;
    
                if (type == null) type = serializableObject.GetType();
    
                System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings();
                settings.OmitXmlDeclaration = true;
                settings.Indent = true;
    
                System.Text.StringBuilder builder = new System.Text.StringBuilder();
                System.Xml.XmlWriter xw = System.Xml.XmlWriter.Create(builder, settings);
    
                System.Xml.Serialization.XmlSerializer x = new System.Xml.Serialization.XmlSerializer(serializableObject.GetType());
                x.Serialize(xw, serializableObject);
    
                return builder.ToString();
            }
    
            /// <summary>
            /// Deserializes workflow markup into an T object
            /// </summary>
            /// <param name="xml">string workflow markup to deserialize</param>
            /// <param name="obj">Output T object</param>
            /// <param name="exception">output Exception value if deserialize failed</param>
            /// <returns>true if this XmlSerializer can deserialize the object; otherwise, false</returns>
            public static bool Deserialize<T>(string xml, out T obj, out System.Exception exception)
            {
                exception = null;
                obj = default(T);
                try
                {
                    obj = Deserialize<T>(xml);
                    return true;
                }
                catch (System.Exception ex)
                {
                    exception = ex;
                    return false;
                }
            }
    
            public static bool Deserialize<T>(string xml, out T obj)
            {
                System.Exception exception = null;
                return Deserialize(xml, out obj, out exception);
            }
    
            public static T Deserialize<T>(string xml)
            {
                System.IO.StringReader stringReader = null;
                try
                {
                    XmlSerializer serializer = new XmlSerializer(typeof(T));
                    stringReader = new System.IO.StringReader(xml);
    
                    return ((T)(serializer.Deserialize(System.Xml.XmlReader.Create(stringReader))));
                }
                finally
                {
                    if ((stringReader != null))
                    {
                        stringReader.Dispose();
                    }
                }
            }
    
            #region private method
            private static string ToString(byte[] ms)
            {
                return Convert.ToBase64String(ms);
            }
    
            private static byte[] ToBytes(string serializedObj)
            {
                return Convert.FromBase64String(serializedObj);
            }
    
            private static byte[] Compress(byte[] buffer)
            {
                MemoryStream ms = new MemoryStream();
                DeflateStream stream = new DeflateStream(ms, CompressionMode.Compress, true);
                stream.Write(buffer, 0, buffer.Length);
                stream.Close();
    
                buffer = ms.ToArray();
                ms.Close();
    
                return buffer;
            }
    
            private static byte[] Decompress(byte[] buffer)
            {
                MemoryStream ms = new MemoryStream();
                ms.Write(buffer, 0, buffer.Length);
                ms.Position = 0;
                DeflateStream stream = new DeflateStream(ms, CompressionMode.Decompress);
                stream.Flush();
    
                byte[] decompressBuffer = new byte[STRING_MAX_LENGTH];
                int nSizeIncept = stream.Read(decompressBuffer, 0, STRING_MAX_LENGTH);
                stream.Close();
                ms.Close();
    
                byte[] lastResult = new byte[nSizeIncept];
                System.Buffer.BlockCopy(decompressBuffer, 0, lastResult, 0, nSizeIncept);
    
                return lastResult;
            }
            #endregion
        }
    }

    接下来要提到的是有趣的利用代码来生成c#文件了,这里面还要提到的就是,messageKey之前的程序员们针对Key的业务SCOPE有写大量region,我不愿意扔掉这个,所以为此在spreadsheet中特地增加了一列为category来手动维护,好在MessageID的命名规则是与这个category有关的,所以这个手动维护不是难事,CodeDom支持Directive,即可以在代码前前面面写statement。唯一坑妈的是,不知道是否是出于什么安全性的考虑,CodeDom居然其实是不支持生成static类的,虽然provider有提供SupportPublicStatic(如代码),但其实是没有用的,而且见鬼的也不支持给member加readonly限制符,所以我只得较tricky的将field name写成"readonly xxxx”了,最后唯一美中不足的就是这个类不是我想要的public static class,我须得手动加上static才好,等想到办法再更新这段(目前木有),

    这段利用System.CodeDom类生成代码的文件就不贴在这儿了,自行下载找GenerateCodeMessageKeyClass.cs

    using System;
    using System.Reflection;
    using System.IO;
    using System.CodeDom;
    using System.CodeDom.Compiler;
    using Microsoft.CSharp;
    using System.Diagnostics;
    using System.Linq;
    
    namespace RefactorMessages
    {
        /// <summary> 
        /// This code example creates a graph using a CodeCompileUnit and   
        /// generates source code for the graph using the CSharpCodeProvider. 
        /// </summary> 
        public class GenerateCodeMessageKeyClass
        {
            public enum DirectiveType
            {
                None,
                OnlyStart,
                OnlyEnd,
                Both,
            }
    
            /// <summary> 
            /// Define the compile unit to use for code generation.  
            /// </summary>
            CodeCompileUnit targetUnit;
    
            /// <summary> 
            /// The only class in the compile unit. This class contains 2 fields, 
            /// 3 properties, a constructor, an entry point, and 1 simple method.  
            /// </summary>
            CodeTypeDeclaration targetClass;
    
            /// <summary> 
            /// Define the class. 
            /// </summary> 
            public GenerateCodeMessageKeyClass(Messages messages, string tower)
            {
                targetUnit = new CodeCompileUnit();
    
                CodeNamespace clsMessageKeys = new CodeNamespace("Util.LoggingSupport");
    
                clsMessageKeys.Imports.Add(new CodeNamespaceImport("System"));
                clsMessageKeys.Imports.Add(new CodeNamespaceImport("Util.Messaging"));
    
                targetClass = new CodeTypeDeclaration(tower.ToString().ToUpper() + "MessageKeys");
                targetClass.IsClass = true;
                targetClass.TypeAttributes = TypeAttributes.Public;
    
                clsMessageKeys.Types.Add(targetClass);
                targetUnit.Namespaces.Add(clsMessageKeys);
    
                foreach (var msg in messages.MessagesByTypes)
                {
                    AddSubClass(msg);
                }
            }
    
            /// <summary> 
            /// Add an entry point to the class. 
            /// </summary> 
            public void AddSubClass(MessagesByType messagesByType)
            {
                CodeTypeDeclaration cls = new CodeTypeDeclaration();
                cls.Name = string.Join("", messagesByType.typeName.Split(' ')) + "Messages";
                cls.IsClass = true;
                cls.TypeAttributes = TypeAttributes.Public;
                cls.Attributes = MemberAttributes.Static;
    
                string comment;
    
                switch (messagesByType.typeCode)
                {
                    case "1":
                        comment = "Validation Error 200~399";
                        break;
    
                    case "2":
                        comment = "Application Error 400~799";
                        break;
    
                    case "3":
                        comment = "System Error 800~999";
                        break;
    
                    case "4":
                        comment = "Info Messages 001~099";
                        break;
    
                    case "5":
                        comment = "Warn Messages 100~199";
                        break;
    
                    default:
                        comment = string.Empty;
                        break;
    
                }
                cls.Comments.Add(new CodeCommentStatement(comment));
    
                var query = messagesByType.Messages.GroupBy(o => o.category);
    
                foreach (var grp in query)
                {
                    int index = 0;
    
                    var list = grp.ToList();
                    foreach (var msg in list)
                    {
                        DirectiveType directiveType = DirectiveType.None;
    
                        if (!string.IsNullOrEmpty(grp.Key))
                        {
                            if (list.Count == 1) { directiveType = DirectiveType.Both; }
                            else if (index == 0) { directiveType = DirectiveType.OnlyStart; }
                            else if (index == list.Count - 1) { directiveType = DirectiveType.OnlyEnd; }
                        }
    
                        AddFields(directiveType, grp.Key, cls, msg);
    
                        index++;
                    }
    
    
                }
    
                targetClass.Members.Add(cls);
            }
    
            /// <summary>
            /// 
            /// </summary>
            /// <param name="flagEndOrStart">-1 start; 0 content; 1 end; 2 both for one field</param>
            /// <param name="cls"></param>
            /// <param name="msg"></param>
            public void AddFields(DirectiveType directiveType, string categoryName, CodeTypeDeclaration cls, Message msg)
            {
                // Declare the widthValue field.
                CodeMemberField field = new CodeMemberField();
                field.Attributes = MemberAttributes.Public | MemberAttributes.Static;
                field.Name = msg.ClassName;
                field.Type = new CodeTypeReference("readonly MessageKey");
    
                string messageType;
    
                switch (msg.TypeCode)
                {
                    case "1":
                        messageType = "MessageType.VALIDATION_ERROR";
                        break;
    
                    case "2":
                        messageType = "MessageType.APPLICATION_ERROR";
                        break;
    
                    case "3":
                        messageType = "MessageType.SYSTEM_ERROR";
                        break;
    
                    case "4":
                        messageType = "MessageType.INFO";
                        break;
    
                    case "5":
                        messageType = "MessageType.WARN";
                        break;
    
                    default:
                        messageType = "MessageType.UNASSIGNED";
                        Debug.Assert(false, "need to modify spreadsheet to specify the type");
                        break;
                }
    
                field.InitExpression = new CodeObjectCreateExpression(
                    new CodeTypeReference("MessageKey"),
                    new CodeTypeReferenceExpression(messageType),
                    new CodePrimitiveExpression(msg.id));
    
                // Add region
                if (directiveType == DirectiveType.OnlyStart || directiveType == DirectiveType.Both)
                {
                    field.StartDirectives.Add(new CodeRegionDirective { RegionMode = CodeRegionMode.Start, RegionText = categoryName });
                }
                if (directiveType == DirectiveType.OnlyEnd || directiveType == DirectiveType.Both)
                {
                    field.EndDirectives.Add(new CodeRegionDirective { RegionMode = CodeRegionMode.End, RegionText = categoryName });
                }
    
                cls.Members.Add(field);
            }
    
            /// <summary> 
            /// Generate CSharp source code from the compile unit. 
            /// </summary> 
            /// <param name="filename">Output file name</param>
            public void Generate(string fileName)
            {
                CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
                provider.Supports(GeneratorSupport.PublicStaticMembers);
    
                CodeGeneratorOptions options = new CodeGeneratorOptions();
                options.BracingStyle = "Block";
    
                using (StreamWriter sourceWriter = new StreamWriter(fileName))
                {
                    provider.GenerateCodeFromCompileUnit(
                        targetUnit, sourceWriter, options);
                }
            }
        }
    }

    最后生成的类会长成这样,using跑到下面去了,当然可以换一种方法来start with,现在是start with namespace,provider还支持GenerateCodeFromExpression,GenerateCodeFromStatement,这样也可以比较"tricky”的先写两行using,再写namespace且不再需要import了。总之CodeDom可以用的场合还有很多,需要用到的时候,google会告诉你的,

    namespace LoggingSupport {
        using System;
        using Util.Messaging;
        
        
        public class MessageKeys {
            
            // Validation Error 200~399
            public class ValidationErrorMessages {
                
                #region AM Common
                public static readonly MessageKey ValCannotBeLessThan = new MessageKey(MessageType.VALIDATION_ERROR, "110");
                
                public static readonly MessageKey ValCannotBeGreaterThan = new MessageKey(MessageType.VALIDATION_ERROR, "111");
                #endregion
            }
            
            // Warn Messages 100~199
            public class WarningMessages {
    	//...
            }   
    }

    做这个小东西,其实蛮有意思的,可惜的是,现在需要为这件事重构的子项目已经不多了,所以用处不是特别大,但至少解放了我 XD,如果在最开始做refactor时我这个小工具就在子项目组中运行,可以省掉大家不少手动copy & paste还有玩瞎眼的烦心事了,好在仍然对维护起作用,是有价值的小工具。

    之后会考虑将这个console project改成个小winform,不然输入一堆path args,挺麻烦的。记录到此,写这篇博客用掉半小时,值!以后有什么小东西,一定要多记录!

    项目源代码下载

    BTW,WLW怎么增加tags?

    愿一路奔跑不退缩,到目前一直从事.Net的B/S,C/S企业应用研发
  • 相关阅读:
    Windows下的RabbitMQ安装:RabbitMQ_Service安装
    [Java]private, public,protected,friendly的区别(转载)一下子就记住了
    mysql的一些sql用法
    set(string str)变量传参为int的问题
    eclipse插件开发常见的问题及解决办法
    eclipse 内存溢出
    如何在一台机器上运行2个Tomcat
    eclipse svn使用
    eclipse遇到的异常
    oracle学习
  • 原文地址:https://www.cnblogs.com/syveen/p/3021238.html
Copyright © 2020-2023  润新知