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<PropertyInfo> GetResourcesMembers()
{
List<PropertyInfo> toGen = 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(null, null);
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/