Making Resources available for other .net dlls


The Problem

I was thinking in have a single common resources Dll for all my libraries to avoid confusion and centralize the localization of them.
But the VS Code Generator for the resource’s files insist in make the properties internal.
I need a way to make them public.

My Solution.

I used T4 template to build a class which exposes the same elements in the resource file as public. Doing so, I can expand the content any time, without the need to write more and more public members.

The basis.

The T4 template helps you creating a file (which could be a class), with the same name of the T4 file and the extension provided in the T4 file definition, using specific directives, like:

<#@ output extension=".cs" #>

(notice the syntax: which start with <#@ and ends with #> )
Inside it, you can write code to generate your class code, using WriteLine or in line tokens for that. (see the official documentation at https://docs.microsoft.com/en-us/visualstudio/modeling/code-generation-and-t4-text-templates ).

Some trick.

To build the different properties for your generated class, the T4 template needs to identify the resources created in the .resx file. As I already told you, they are internal and cannot be accessed from outside the dll. The T4 generator is, in fact, outside the project.
However, the text transformation process (the “generator”), is capable to use Reflection to reach public members like any other .Net code. We just a need to query the resource members rom outside… and that could be done by creating a public class to which we can call and retrieve the members.
At the same time, we can use it to have other practical methods for reach our resources.
Using the partial class feature, we can define the code we need in a partial class which could be part of the generated one.
Let consider we will generate a “Resources.cs” class. We need another file, with some kind of extended extension (I use this name to define a file with more than one dot in its name).
In this case, we have to add a class named, as example, Resources.Tools.cs where we build a public function capable to get the members names of the resource file,. Something like this code:

public partial class Resources
{
   #region "Generator helper"
   public static List<PropertyInfoGetResourcesMembers()
   {
      List<PropertyInfotoGen = new List<PropertyInfo>();
      var res = (from TypeInfo item in System.Reflection.Assembly.GetExecutingAssembly().GetTypes() where item.IsNotPublic orderby item.Name select item.AsType()).ToList();
      foreach (var item in res)
      {
         toGen.AddRange(item.GetProperties(BindingFlags.NonPublic | BindingFlags.Static).OrderBy(x => x.Name).Where(x => x.Name != "ResourceManager"));
      }
      return toGen;
   }
   #endregion "Generator helper"
}

The function analyzes the classes Not Public in the project dll and retrieves the list of properties for all of them.
Once we have this dll compiled, we can proceed to create our T4 template, adding a file named Resources.tt (the tt is the extension for T4 templates).
I’ll no go into the details of all the T4 syntax but, believe me, you need this list of references and directives:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Reflection" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE80" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #> 
<#@ import namespace="EnvDTE" #>
<#@ import namespace="EnvDTE80" #>
<#@import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#@import namespace="System.IO" #>
<#@import namespace="System.Xml.Linq"#> 
<#@ output extension=".cs" #>

And here you have the entire explained code:

Snippet

<#
    // retrieve the Visual studio Environment
    DTE dte = ((IServiceProvider)(this.Host)).GetCOMService(typeof(DTE)) as DTE;
    //  Get the projects in current solution
    IList<Project> Projects=GetProjects(dte);
    string debugPath="";
    // define thew name of the class to build (and the partial class we have already created with our helper function)
    string className=Path.GetFileName(this.Host.TemplateFile).Replace(".tt","");
    // Get the directory where the template is stored
    DirectoryInfo projectDir = new DirectoryInfo( Path.GetDirectoryName(this.Host.TemplateFile));
    // Obtain the project file itself
    FileInfo projectFile=( from el in projectDir.GetFiles("*.csproj"select  el).FirstOrDefault();
    // Cleans the project name to search it into the projects' list of the solution
    string projectName=projectFile.Name.Replace(".csproj","");
    // Get the Project object
    Project resourcesPrj=(from el in Projects where el.Name==projectName select el).FirstOrDefault();
    // Gets the directory where the project is stored
    string filePath=(from Property prop in resourcesPrj.Properties where prop.Name=="LocalPath" select prop.Value.ToString()).FirstOrDefault();
    // Obtains the Default name space of the project
    string namespaceName=(from Property prop in resourcesPrj.Properties where prop.Name=="DefaultNamespace" select prop.Value.ToString()).FirstOrDefault();
    // obtains the file name of the compiled the binaries
    string outputFile=(from Property prop in resourcesPrj.Properties where prop.Name=="OutputFileName" select prop.Value.ToString()).FirstOrDefault();
    // Reads the project fiel as XML
    XElement xel = XElement.Load(resourcesPrj.FullName);
    // Find the TargetFramewroks entry to see it it is netStandard or not (the binary is stored in different path)
    XElement fwk = xel.Descendants("TargetFrameworks").FirstOrDefault();
    // Defines the destination folder for the debug binaries depending on the value of TargetFrameworks
    if (fwk !=null && fwk.Value.Contains("netstandard2"))
    {
        debugPath = @"\bin\Debug\netstandard2.0\";

    }
    else
    {
        debugPath = @"\bin\Debug\";;
    }
    // Configure the full path for the binaries
    filePath+=debugPath+outputFile;
    // Loads the latest version of the binaries as an assembly (to get the up to date list, you will need to compile once BEFORE generate the class with this template)
    System.Reflection.Assembly ass=System.Reflection.Assembly.Load(System.IO.File.ReadAllBytes(filePath));
    try
    {
        // Get the type already created with the same name than this template (the first time, it will contains ONLY the support function)
        Type tr = ass.GetType(namespaceName + "." + className);
        // Calls the support method we created, to get the list of the entire resources
        List<System.Reflection.PropertyInfo> toGen = (List<System.Reflection.PropertyInfo>)tr.GetMethod("GetResourcesMembers").Invoke(nullnull);
        if (toGen.Count > 0)
        {
            // start writin g the result file, stariting by its namespace
            WriteLine("namespace " + namespaceName);
            WriteLine("{");
            // define the class (as partial to be combined with the .Tools.cs file during the build of the dll)
            WriteLine("public static partial class " + className);
            WriteLine("{");
            Type prevType = null// this variable store from which type are we getting resources names. THe first time will be null, and will be defined in the following lines
            foreach (System.Reflection.PropertyInfo item in toGen)
            {
                if (prevType != item.DeclaringType) // DeclaringType contains the type where this PropertyInfo belongs to
                {
                    if (prevType != null)// in case we already have a previous type, we ar in a change of type, so we need to close the current generated class
                    {
                        WriteLine("}");
                    }
                    prevType = item.DeclaringType; //<= HERE we store the type from witch we are getting members
                    WriteLine("public static class " + prevType.Name + "{"); // defines a nested class with the DeclaringType name

                }
                WriteLine(string.Format("public static {0} {1} => {3}.{2}.{1};", item.PropertyType, item.Name, item.DeclaringType.Name, namespaceName)); // writes the public declaration of the property, which internally calls the internal member
            }
            // closes the final curly brackets
            WriteLine("}");
            WriteLine("}");
            WriteLine("}");
        }
    }


    catch (Exception ex)    {
        var ufa = ex.Message;
    }
    finally
    {
        ;


    }
        
        
#>
    <#+
    // NOTICE: The syntax for functions includes a plus sign
    // It is hard to get the projects inside a solution, since there could be folders etc. in the solution.
    // See https://wwwlicious.com/envdte-getting-all-projects-html/ where I get this part of the code.(Thanks Scott)
    public  IList<Project> GetProjects(DTE dte)
    {
        Projects projects = dte.Solution.Projects;
        List<Project> list = new List<Project>();
        var item = projects.GetEnumerator();
        while (item.MoveNext())
        {
            var project = item.Current as Project;
            if (project == null)
            {
                continue;
            }
    
            if (project.Kind == ProjectKinds.vsProjectKindSolutionFolder)
            {
                list.AddRange(GetSolutionFolderProjects(project));
            }
            else
            {
                list.Add(project);
            }
        }

        return list;
    }

    private  IEnumerable<Project> GetSolutionFolderProjects(Project solutionFolder)
    {
        List<Project> list = new List<Project>();
        for (var i = 1; i <= solutionFolder.ProjectItems.Count; i++)
        {
            var subProject = solutionFolder.ProjectItems.Item(i).SubProject;
            if (subProject == null)
            {
                continue;
            }

            // If this is another solution folder, do a recursive call, otherwise add
            if (subProject.Kind == ProjectKinds.vsProjectKindSolutionFolder)
            {
                list.AddRange(GetSolutionFolderProjects(subProject));
            }
            else
            {
                list.Add(subProject);
            }
        }
        return list;
    }
#>

A final note

It is not so easy to get the projects inside the solution, since there could be a mess of folders, and other kind of files defined in a .sln file.
I want to thanks Scott Mackay for publish this useful entry: https://wwwlicious.com/envdte-getting-all-projects-html/

Anuncio publicitario

Comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.