Semi-automatic conversion of laziness to code

    Good day to all. Today I want to talk about automatic C # code generation. For example, properties in classes describing entities of a subject domain are usually described in exactly the same way. And it’s elementary for me to be lazy to write the same constructions for each primitive property. The use of snippets and active templates saves a little, but when the need comes to change something in this scheme, you have to shovel a bunch of code. So why not generate this monotony automatically during the build process?
    At some point, the kinetic energy of creativity briefly overpowered the potential energy of laziness, and the result was a small library for automatically generating some program source files based on external data. I invite all the lazy (in the good sense of the word) C # developers to cat.


    Prologue


    When I was programming in C ++, I used the #define directive with parameters for these purposes. A great mechanism that allows you to declare a piece of code in a simple line with inserts, which you can then reuse many times. This is not at all the same as templates or generic classes offer.
    Unfortunately, C # developers did not include support for the #define directive, similar to C ++. There are probably reasons for this, primarily in the readability and transparency of the code. But I still wanted to be able to avoid the need for code duplication when declaring uniform constructions.
    I must say that Microsoft is quite actively using code generation. Take at least Linq2Sql classes - all declarations of the class structure are in the xml file on the basis of which the code is generated. Moreover, the .NET Framework includes a whole System.CodeDom namespace dedicated to code generation. Numerous methods and classes allow you to generate code in terms of the CLR and save it in the form of any language supported by .NET - C #, VB.Net. It looks something like this:

            /// 
            /// конструирует простое свойство-значение
            /// 
            /// 
            /// 
            private void CreateProperty(CodeTypeDeclaration newC, XmlElement e)
            {
                string propName = e.GetAttribute("name");
                var propType = ResolveType(e.GetAttribute("datatype"));
                var rel_field_prop_name = createPropertyConstName(newC, e, propName);
                var new_prop = new CodeMemberProperty
                  {
                      Attributes = MemberAttributes.Public | MemberAttributes.Final,
                      Name = "p_" + propName,
                      HasGet = true,
                      HasSet = true,
                      Type = propType
                  };
                var comment = e.GetAttribute("displayname");
                if (!string.IsNullOrEmpty(comment))
                    new_prop.Comments.Add(new CodeCommentStatement("\n " + comment + "\n ", true));
                new_prop.GetStatements.Add(new CodeMethodReturnStatement(
                    new CodeCastExpression(propType,
                        new CodeMethodInvokeExpression(
                            new CodeMethodReferenceExpression(new CodeBaseReferenceExpression(), "GetDataProperty"),
                            new CodeFieldReferenceExpression(null, rel_field_prop_name)))));
                new_prop.SetStatements.Add(
                        new CodeMethodInvokeExpression(
                            new CodeMethodReferenceExpression(new CodeBaseReferenceExpression(), "SetDataProperty"),
                            new CodeFieldReferenceExpression(null, rel_field_prop_name), new CodePropertySetValueReferenceExpression()));
                newC.Members.Add(new_prop);
            }
    


    I used this mechanism several times to solve particular problems, and came to the conclusion that it is not suitable for my task. After all, I need to be able to quickly slip a piece of code to substitute, and the methods mentioned generate code based on its structure. Those. To use the built-in tools, you first need to write a template translator that will parse the C # code to generate it on its basis. Complete nonsense.


    Also, it should be noted that Visual Studio includes such an opportunity as the ability to create your own languages ​​and visual editing tools for them. You can read more about this here . Very interesting, but terribly cumbersome.

    In 2010, Microsoft came up with T4 Text Templates . Judging by the documentation, this is almost what you need. But, firstly, I still have projects at the 2008th studio, and secondly, I was not able to get them to work :-(.

    There is still such a thing as Nemerle . There is generally the opportunity to come up with your own language on top of C #. Cool, but again not what you need,
    because I just want to be able to reuse pieces of C # code.

    I want something simple


    So, I reached a state of readiness - my hands are already itching to program something. The main Wishlist are formulated:
    1. Code-generating code should be simple and easy to read.
    2. Classes for code generation must implement Linq-style chain interfaces.
    3. The template that is used for generation must be declared as a simple string.
    4. It should be possible to combine manually generated and written code in one class

    And based on them, technical solutions are developed:
    1. A library is made containing classes and methods for simple code generation
    2. The Solution includes a project of an executable application, which is one of the first to be assembled, immediately launched (in PostBuildStep) and generates the necessary parts for other projects using the mentioned library.
    3. To be able to combine the generated and written code, we use partial classes. (Probably, this feature was added to C # so that it would not be so offensive due to the lack of #define ).
    4. We draw up the initial data for code generation, for example, in the form of transfers. Why? Yes, it's just convenient.

    Bottom line - the code for generating a class with a set of similar properties looks something like this:

    //объявляем шаблон свойства
    const string CommandPropertyTemplate =
    @"public static 
    {{
        get
        {{
            return PVCommand..GetCommand();
        }} 
    }}
    ";
    //конструируем исходный файл со статическим классом
     var commandsClass = CodeWriter
         .BeginSource("Commands.cs")
         .Using("MyApp.Display")
         .BeginNamespace("MyAppCommands")
         .AddClass("Commands").Static();
    //для каждого элемента перечисления добавляем одноименное свойство в класс
    //используя одинаковый шаблон
    var allcmds = System.Enum.GetValues(typeof(PVCommand)).Cast();
    foreach (var cmd in allcmds)
    {
        commandsClass.AddBlock(cmd.ToString(), CommandPropertyTemplate);
    }
    

    A few comments about the design of the template. Double curly braces are used to simply render this string to string.Format. Keywords and are replaced with the name and type of the property, which are passed as parameters to the AddBlock () method.

    So why is all this necessary?


    Well, now let's think a little bit about how all this can be applied.

    Work with localizable strings

    In one of the projects, I had the task of localizing a WPF application, which was solved in this way . However, adding each new line required a large, uniform chunk to be inserted into the XAML. When I started the next project, I decided to improve the solution using this library. So, an enumeration that contains the keys of string resources is fed to the input, and the values ​​for the neutral locale are placed in the Description attribute:
        public enum Strings
        {
            [Description("MyCoolApp - trial version")]
            AppTitle,
        }
    


    Based on this enumeration, two artifacts are generated:
    1. XAML with resources. (Classes from Linq2XML are used to generate it)
    2. Static class for convenient access to resources from code.

    Generating all the code needed to work with localizable strings in WPF looks like this:
    //шаблон для свойства класса
    const string ResourceEntry =
    @"
    public const Key = """";
    public static  {{
        get{{
            return App.RString(Key);
        }}
    }}
    ";
    //объявляем статический класс для ресурсов
    var stringTable = CodeWriter
        .BeginSource("Strings.cs")
        .BeginNamespace("MyApp")
        .AddClass("StringTable")
        .Static();
    //добавляем свойства для доступа к ресурсам
    foreach (var rstring in GetResourceStrings())
    {
        var resourseKey = rstring.ToString();
        stringTable.AddBlock(resourseKey, ResourceEntry);
    }
    //функция для генерации XAML
    void StringTableXamlTo(string dir)
    {
        var nsDefault = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
        var nsX = "http://schemas.microsoft.com/winfx/2006/xaml";
        var nsCore = "clr-namespace:MyApp.Core;assembly=MyApp.Core";
        var resourcedict = new XElement(XName.Get("ResourceDictionary", nsDefault),
            new XAttribute(XName.Get("Uid", nsX), "StringTable"), 
            new XAttribute(XNamespace.Xmlns + "core", nsCore),
            new XAttribute(XNamespace.Xmlns + "x", nsX));
        foreach (var rstring in GetResourceStrings())
        {
            var resourseKey = rstring.ToString();
            var resourceValue = EnumHelper.GetDescription(rstring);
            var resourceDecl = new XElement(XName.Get("StringObject", nsCore),
                new XAttribute(XName.Get("Uid", nsX), "UID_" + resourseKey),
                new XAttribute(XName.Get("Key", nsX), resourseKey),
                new XAttribute("Localization.Attributes", 
                            "Value (Readable Modifiable Text)"),
                     new XAttribute("Value", resourceValue)
                     );
                 resourcedict.Add(resourceDecl);
             }
             var xaml = new XDocument(resourcedict);
             xaml.Save(Path.Combine(dir, "stringtable.xaml"));
         }
    }
    

    Now, to add a new line to resources, you just need to add an element to the enumeration (of course, you must remember to specify an attribute with the original value). Translation, in accordance with the technology, can be added later.

    Domain Model

    Another example. So I have classes that model the entities of the subject area. Each class implements the INotifyPropertyChanged interface. Each of them has properties. These properties are all arranged in the same way - a hidden field that stores the value of the property, getter simply returns the value of the property, and setter changes the value of the field and generates a notification of the change. Previously, I wrote each such property with my hands. Then I learned to embed code in a pattern. And now I want to describe the template in one place, and list the properties with their types. We declare the list of class properties, again, as an enumeration. To each element of the enumeration, add an attribute that describes the type of property. Generating class code will be almost no different from the examples above, you only need to read the type of the property from the corresponding attribute.
    const string ModelPropertyTemplate = 
    @"
     _;
    public 
    {{
        get
        {{
            return _;
        }}
        set
        {{
            _ = value;
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(""""));
        }}
    }
    "; 
    


    Conclusion


    The resulting solution allowed me to save a lot of time, and even more nerves. After all, when it was necessary to add the generation of additional actions to the property template, it was not difficult. In the previous project, when the code was static, I couldn’t decide on its global and uniform refactoring. And it really got on my nerves.
    The undoubted disadvantages of the solution include the need to separate code generation into a separate project. Unfortunately, while it was not possible to combine everything in one project, I will be grateful for the ideas.

    Thanks for attention!

    PS Here you can familiarize yourself with the sources of the mentioned library.

    Also popular now: