Saturday, January 28, 2006

Unsealing Visual Studio Generated Properties.Settings

I've often wanted to have public access to the Properties.Settings classes generated by Visual Studio 2005 because:
  1. I want to inherit from the Settings to add special helper methods to the generated code.
  2. I want to write external unit tests that access the Properties.Settings, but they are internal.
  3. I also want to inherit from Setting to add a buffer layer between my code and the generated code. In case I need to fix things later, when Microsoft changes the generator.

At first, I thought: There's no real good way to do the above without manually editing generated code.

But then I learned more about the "Custom Tool" property that shows when you click F4 (Properties) on the Application ... Properties... Settings.settings node in the project tree. By default this will say: "SettingsSingleFileGenerator"

What is "SettingsSingleFileGenerator"? It's a .NET object that exposes a special COM interface. The .NET object runs and generates code (from XML settings in this case), which you see in the generated code node underneath: Settings.Designer.cs.

Turns out you can make your own generators and then install them into visual studio. But, it would be a pain to re-create the SettingsSingleFileGenerator. Luckily, you can chain these generators together: You just set the CustomTool properties of the previously generated code to use your custom generator. Nice!

Like in this screen shot:





If the instructions in the two links above make sense, then here is the code for a generator that will unseal your settings.

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;

using CustomToolGenerator;

// Code downloaded from here: http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=4AA14341-24D5-45AB-AB18-B72351D0371C
// Install notes: http://www.drewnoakes.com/snippets/WritingACustomCodeGeneratorToolForVisualStudio/
namespace UnsealedSettingsGenerator
{
[Guid("828BA458-f1f1-f1f1-f1f1-91ACEC2F38FD")]
public class UnsealSettingsGenerator: BaseCodeGeneratorWithSite
{

protected override byte[] GenerateCode(string inputFileName, string inputFileContent)
{
// Replace the source from the input, with our new text
// This is lamer than parsing, but it does the job and makes the point.
inputFileContent = inputFileContent.Replace("internal sealed", "public");
return Encoding.ASCII.GetBytes(inputFileContent);
}

public override string GetDefaultExtension()
{
return ".Unsealed" + base.GetDefaultExtension();
}
}
}

A key point is to make sure you set the registry keys right (see screen below), so VS2005 will load your generator. Also, you can right click on the source file and choose "Run Custom Tool" to force your generator to run.



This is all a lot of work to generate and install. I have some code that lets create your own generators easily and keep the "generator's code" (not the generated code) in the same project you're running in (without all the COM malarkey). Maybe I'll upload that sometime.

13 comments:

Anonymous said...

Hello there,
This is something I've been looking for quite a while. The idea is great and could be used for various generators- the only problem is that I seem to get something wrong; I followed your steps, and those of http://www.drewnoakes.com/snippets/WritingACustomCodeGeneratorToolForVisualStudio/
yet for some reason I get this weird xml looking file whenever using the new generator, instead of a Desiner file. Any ideas?

Anonymous said...

Oopsi! I was trying the generator on the original rather than the generated file...
Anyway, great job!!

Anonymous said...

Hope I'm not flooding you here, but there is something I can't work out with this- whenever I build my project I get an error since the second degree generated Settings file (the .Unsealed one) is public while the parent class is private (and cannot be turned into public as well). What did you do to solve this?

Jorge Monasterio said...

I appreciate the feedback. I'll dig up the code and post here. Or you can AOL IM me at: JorgeAtWork.

Jorge Monasterio said...

I think this may be the problem:

On the first generated file (I guess you would call it the first degree), look at the properties window and make sure the "Build Action" property is set to NONE.

On the xxx.unsealed.cs, make sure the "Build Action" is set to "compile".

Good luck.

Anonymous said...

You're a life saver!! This is pure loveliness! cheers mate :)

Unknown said...

Thanks for the post, It worked like a charm for me. Do you have the code posted somewhere that lets you do this, as you say: "without all the COM malarkey"? This seems like quite a bit of work for something so simple.

Jorge Monasterio said...

JUSTIN:

Basically, to workaround having to COM/register a bunch of tools for each generator I wanted to make, I did this:

I do register one tool as described in the article. I call this tool, Preprocessor. This tool looks in the "Custom Tool NAMESPACE" property (remember how the generator itself is put in the "Custom Tool" property) of the settings file, where I have typed in some extra information: The path of a DLL followed by a comma and the name of a class.

So what the Preprocessor tool does, is load the specified DLL and looks for the specified class in that DLL.

The class is a regular .NET class. So you don't have to make it into a a COM object or register it.

Here is code for preprocessor.cs (goes in one project):

// Code downloaded from here: http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=4AA14341-24D5-45AB-AB18-B72351D0371C

// Install notes: http://www.drewnoakes.com/snippets/WritingACustomCodeGeneratorToolForVisualStudio/

// Special code generator that can load other code generators...
namespace Preprocessor
{
[Guid("728BA458-f1f1-f1f1-f1f1-91ACEC2F38FE")]
public class PreprocessorGenerator : BaseCodeGeneratorWithSite
{
public override byte[] GenerateCode(string inputFileName, string inputFileContent)
{
// Dynamically load the right tool and call it here based on configuration passed in the "custom tool name
// space." So I don't have to register a bunch of different tools.
// Load the assembly.
//
//
// For example: CustomToolNameSpace setting contains:
// c:\src\youHavefiles\generators\bin\debug\Generators.dll,Generators.UnsealedSettingsGen
//
// So here we parse out this info...
Console.WriteLine("Params: " + this.FileNameSpace);
string[] arrParams = this.FileNameSpace.Split(',');
string dll = arrParams[0];
Console.WriteLine("dll name: " + dll);
string className = arrParams[1];
Console.WriteLine("Class name: " + className);
//return Encoding.ASCII.GetBytes("Hmmm Class name: " + className);


//int code = domain.ExecuteAssembly(dll);
Assembly asm = Assembly.LoadFrom(dll);

// Get the type.
Type type = asm.GetType(className);

// Get the instance, but get an instance of the interface by casting.
BaseCodeGeneratorWithSite instance = (BaseCodeGeneratorWithSite)Activator.CreateInstance(type);

byte[] ret = instance.GenerateCode(inputFileName, inputFileContent);
return ret;

}

public override string GetDefaultExtension()
{
return ".Unsealed" + base.GetDefaultExtension();
}
}
}

AND Then you just make a DLL that has a method with this public signature.

public override byte[] GenerateCode(string inputFileName, string inputFileContent)


I hope this helps.

Unknown said...

Adding a question to a post from ancient times, hoping someone will see it and answer ;)

I get this to work fine in VS2005, when I do the same in Visual C# 2008 Express however, it doesn't work.

I suspect it reads its info about generators from a different registry key, but where?

For VS2005 I use:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\Generators

I thought the corresponding registry path for 2008Express was:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\Generators since that path existed, and I have never had standard VS2008 installed on this computer. But 2008Express gives me the error "Cannot find custom tool".

Does anyone have a clue?

Unknown said...

Why is it that you always find the solution yourself right after asking for help?

Anyway, Visual C# 2008 Express store its data in:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VCSExpress\9.0\Generators

Jorge Monasterio said...

Glad you found a solution, because I haven't tried on 2008.

Charlie said...

Does anyone get this error with trying to override GenerateCode?

Cannot access protected member 'Microsoft.CustomTool.BaseCodeGenerator.GenerateCode(string, string)' via a qualifier of type 'Microsoft.CustomTool.BaseCodeGeneratorWithSite';

In my BaseCodeGeneratorWithSite, GenerateCode is signed:

protected abstract byte[] GenerateCode(string inputFileName, string inputFileContent);

Is there a different version of this dll that I should be using?

Charlie said...

I ended up doing the following to call the protected GenerateCode method:

protected override byte[] GenerateCode(string inputFileName, string inputFileContent) {
MethodInfo oMethod = this.Instance.GetType().GetMethod("GenerateCode", BindingFlags.Instance | BindingFlags.NonPublic);
if (oMethod != null) {
return oMethod.Invoke(this.Instance, new object[] { inputFileName, inputFileContent }) as byte[];
}
return null;
}