星期日, 十一月 04, 2007

Visual Studio designer, CodeDom and InitializeComponent()



 
 

Hudong 通过 Google 阅读器发送给您的内容:

 
 

于 07-11-4 通过 MSDN Blogs 作者:carloc

The code problem

A few days ago I had the chance to work on a case not concerning ASP.NET or IIS, but rather an interaction with Visual Studio designer with classes from System.CodeDom namespace. The customer was developing a custom control meant for other developers to use it in their projects, and upon adding the control to the form, the former had to automatically modify the some codebehind files for example to add some custom methods and register itself in the InitializeComponent method to add events etc... The problem was that we were able to add new methods to the form, but despite our attempts nothing was added to InitializeComponent smile_thinking

codedom_initialize_menu

Here's the chunk of code we were using:

private void AddCode(Form form) {    IDesignerHost host = null;    host = (IDesignerHost)form.Site.GetService(typeof(IDesignerHost));     //Add method "Form1_Load" on Form1    //---------------------------------------------------------------------------    CodeMemberMethod member = new CodeMemberMethod();    member.Name = "Form1_Load";    member.Parameters.Add(new CodeParameterDeclarationExpression("System.Object", "sender"));    member.Parameters.Add(new CodeParameterDeclarationExpression("System.EventArgs", "e"));    CodeSnippetExpression sn;    sn = new CodeSnippetExpression("MessageBox.Show(\"Hello world\")");    member.Statements.Add(sn);    member.Attributes = MemberAttributes.Private;    CodeTypeDeclaration typedecl = (CodeTypeDeclaration)form.Site.GetService(typeof(CodeTypeDeclaration));    typedecl.Members.Add(member);    //---------------------------------------------------------------------------      //This code will add the following line to the "InitializeMethod" method    // this.Load += new System.EventHandler(this.Form1_Load);    //---------------------------------------------------------------------------    member = new CodeMemberMethod();    foreach (CodeTypeMember typememb in typedecl.Members)    {        if (typememb.Name == "InitializeComponent")        { member = (CodeMemberMethod)typememb; }    }    CodeDelegateCreateExpression createDelegate1;    createDelegate1 = new CodeDelegateCreateExpression(new CodeTypeReference("System.EventHandler"), new CodeThisReferenceExpression(), "Form1_Load");    CodeAttachEventStatement attach = new CodeAttachEventStatement(new CodeThisReferenceExpression(), "Load", createDelegate1);    member.Statements.Add(attach);    typedecl.Members.Add(member);    //---------------------------------------------------------------------------      //Add and remove a label because otherwise the code to add the method seems to stay "inactive,    //while in this way it works    //---------------------------------------------------------------------------    Label lbl = (Label)host.CreateComponent(typeof(Label));    host.DestroyComponent(lbl);    //--------------------------------------------------------------------------- }

Interestingly beside InitializeComponent Visual Studio was showing the yellow line which means that the code has been changed and still needs to be saved, so something was actually happening... so I tried to add the event in another method and it worked fine; an also adding a MessageBox inside the foeach loop to display LinePragma.FileName and LinePragma.LineNumber showed we were working on the appropriate code file and code line (the InitializeComponent declaration)... So there is a problem with InitializeCompoent itself?

Well, yes.

I remember I read about it long time ago when I was getting my hand dirty with the emerging .NET technology (it was back in 2001 I think), but then I moved to web applications and this went somewhere in the back of my mind... the InitializeComponent is a very important method for Visual Studio, which protects it from custom manipulations which might compromise good working of the designer, and this is actually explained in a KB article: Code comments that you add in the InitializeComponent method are lost when you add a control in Visual Studio .NET or in Visual Studio 2005.

And that was actually the exact situation we where into: for some reason the code was not added as expected unless we were adding and removing a control to the From (a Label in our case) and this simple trick had the effect to somehow activate the code (I guess that way we were calling some specific method otherwise we were not touching), but his also had the side effect to put us in the situation described in the article. And even if our was were working fine, sooner or later we would have stumbled in this kind of Visual Studio protection anyway, because a developer would have certainly added other controls to the form anyway...

So we had two problems here:

  1. Find a way to add event lines to the Form initialization
  2. Find a way to "refresh" the code without adding/removing fake controls to the Form

To resolve point one we decided to create an InitializeComponent2 method still inside Form1.Designer.cs (to have it consistent with the default InitializeComponent added by Visual Studio) so we were now able to add our events and custom properties, and add a call to our InitializeComponent2 in Form1 construction, right below the standard InitializeComponent() call, and this way we had our event working as expected smile_regular.

Chain of Events

When using design-time controls, it's important to know what happens behind the scenes when you drop a control on your design surface—or for controls such as a Form, what happens when you create a Form that is inherited from another Form you created.

When an object is opened in the design environment, the class that the object is inherited from (not the newly created class) is constructed by Visual Studio. Remember, a control that is created fires its constructor, which also fires the InitializeComponent() method within the base class's constructor. Once the derived class is constructed, that magically named method within your newly created class, InitializeComponent(), is parsed line-by-line by Visual Studio. The only thing magic about this method is its name. Visual Studio just knows to look for this method.

In Visual Basic .NET, if Visual Studio can't understand the line of code, the line is removed (a nice way of saying eaten). The Visual Basic team felt it was more important to maintain the design-time environment. The C# team felt it was more important to maintain the code, so if Visual Studio can't parse InitializeComponent() you'll get the text of the exception presented to you. This includes inherited Forms, or controls placed on the design surface of another component class control.

Any property that is part of your base object is set by the base object and its constructor. When the InitializeComponent() method runs, the values are changed to the values you have set on the property sheet. In Visual Studio, the property sheet is just a graphical representation of the InitializeComponent() method. If your base class performs some sort of functionality in the constructor, be careful; it will be executed in the design environment as well.

You do have some control over this, however. Any class that derives from the Component class has a property called DesignMode that is set to true when the code is executing within the constructs of the Visual Studio designer. So you have the option to wrap code within an if statement. There's one more trick, however. The DeisgnMode property isn't set to true in the constructor. Remember, there's no real magic here. Visual Studio creates your object as it parses the InitializeComponent() method. Once the object is constructed, Visual Studio keeps track of the objects it creates, and simply says:

newlyCreatedObject.DesignMode = true

Next steps

The argument intrigued me, so I was curious to further explore the capabilities offered by the CodeDom namespace; doing some researches on the Internet you can quite easily find articles which describes how to create a class from crash, create the source file and compile it on the fly to have the resulting assembly and this is quite a standard usage of CodeDome, here are just a couple of references:

But I've not been able to find good articles about how to customize the current code graph, the one we're currently using (instead of creating a completely new class/component from scratch), so I made some tests myself with conflicting results (something's working the way I like, something don't...). Here's what I've come up with, it's still not perfect (continue reading to find out what's wrong) but even if far from being perfect at least it works:

private void AddCode(Form form) {     //Hook to the designer     IDesignerHost host = (IDesignerHost)form.Site.GetService(typeof(IDesignerHost));        //Add method "Form1_Load" on Form1     CodeMemberMethod member = new CodeMemberMethod();     member.Name = "Form1_Load";     member.Parameters.Add(new CodeParameterDeclarationExpression("System.Object", "sender"));     member.Parameters.Add(new CodeParameterDeclarationExpression("System.EventArgs", "e"));     CodeSnippetExpression sn = new CodeSnippetExpression("MessageBox.Show(\"Hello world\")");     member.Statements.Add(sn);     member.Attributes = MemberAttributes.Private;     CodeTypeDeclaration typedecl = (CodeTypeDeclaration)form.Site.GetService(typeof(CodeTypeDeclaration));     typedecl.Members.Add(member);     //---------------------------------------------------------------------------        //Create an InitializeComponent2() method     member = new CodeMemberMethod();     member.Name = "InitializeComponent2";     typedecl.Members.Add(member);     //---------------------------------------------------------------------------        //Find the constructor and add a call to my "InitializeComponent2()" method     //CodeConstructor ctor = new CodeConstructor();     CodeConstructor ctor = null;     foreach (CodeTypeMember typememb in typedecl.Members)     {         if (typememb.Name == ".ctor")         {             ctor = (CodeConstructor)typememb;             break;         }     }      CodeMethodInvokeExpression invokeExpression = new CodeMethodInvokeExpression();     invokeExpression.Method = new CodeMethodReferenceExpression(         new CodeThisReferenceExpression(), "InitializeComponent2");      ctor.Statements.Add(invokeExpression);     typedecl.Members.Add(ctor);     //---------------------------------------------------------------------------        //This code will add the following line to the "InitializeMethod2" method     // this.Load += new System.EventHandler(this.Form1_Load);     member = new CodeMemberMethod();     foreach (CodeTypeMember typememb in typedecl.Members)     {         if (typememb.Name == "InitializeComponent2")         {             member = (CodeMemberMethod)typememb;         }     }      CodeDelegateCreateExpression createDelegate1 = new CodeDelegateCreateExpression(         new CodeTypeReference("System.EventHandler"), new CodeThisReferenceExpression(), "Form1_Load");     CodeAttachEventStatement attachStatement1 = new CodeAttachEventStatement(new CodeThisReferenceExpression(), "Load", createDelegate1);      member.Statements.Add(attachStatement1);     typedecl.Members.Add(member);     //---------------------------------------------------------------------------        //Add and remove a label because otherwise the code to add the method seems to stay "inactive",     //while in this way it works     Label lbl = (Label)host.CreateComponent(typeof(Label));     host.DestroyComponent(lbl);     //--------------------------------------------------------------------------- }

The debug problem

Debugging design time controls is not part of my daily job (I've never saw one, in web development), so while working on this case I had to figure out out to debug one of those... At the beginning I just needed to check a couple of variable values so to make things simple I just added a couple of MessageBox.Show() where I needed (like I used to fill my old ASP pages with Response.Write() statements long time ago). As you can guess this is not a practical approach. And at the same time I could not imagine to be the only person facing this problem, so there must be some article outside on the Internet dealing with this issue... and here is it, just in case you need it: Walkthrough: Debugging Custom Windows Forms Controls at Design Time.

Essentially what needs to be done is set the control's project as the startup progect, change the debug start action for the control you need to debug, setting it to "Start external program" and pointing it to the Visual Studio executable (default path is C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe), then set a breackpoint where you want to stop in its code and press F5 to start debugging. A new instance of Visual Studio is created, and from it you can open once again the Solution which contains the control you want to debug; now simply do what you need to run the control (add it to a new form for example) and hit the breakpoint (within the first Visual Studio instance, of course). Here we are, now you can step through, inspect variables and do whatever you usually do while debugging. smile_nerd

Note that the breakpoint icon in the first instance of Visual Studio will display the "breakpoint not hit" icon and tooltip until you cause the code to run, through the second Visual Studio instance

What's still missing

To have a well designed component I wanted to be able to create the InitializeComponent2 method where I wanted (possibly in the Designer.cs file to have it together with the standard InitializeComponent created by Visual Studio), remove the "#line" entries in the class constructor to have the code clean and easy to read and avoid the ugly trick to ad/remove a label to the form for activate the code. Unfortunately those are three problems I've still not resolved smile_thinking.

Creating a new method in Form1.cs is not a big deal, nor is adding some lines of code to it; but problems arise when trying to interact with the class constructor. This is not a protected method as we saw for InitializeComponent(), but no matter what I tried, there are always a few "#line" statements added:

public Form1() {  #line 17 "C:\Temp\TestForMS\TestForMS\TestForm\Form1.cs"     this.InitializeComponent();  #line default #line hidden     this.InitializeComponent2();  }

Interestingly those nasty "#line" does not appear if we create a new code constructor, I guess that's because in that case we're working with a new object we created and have "full control" on it; to make an example, creating a constructor overload and adding a call to my InitializeComponent2() produces perfectly clean code. Based on this assumption (still to verify, anyway) I tried to replace the class constructor with a new object created for this purpose, but when adding it to the class code graph still produces those lines... smile_eyeroll. Those are C# Preprocessor Directives and luckily the compiler does not complain, so for the moment I had to give up and live with them... smile_sad; but that's just for now, hopefully I'll find a way to get rid of them.

Ideally I wanted to create my InitializeComponent2() method to the Form1.Designer.cs file, but the default behavior is to add it to the Form1 class (in Form1.cs); the nice thing is that we have a LinePragma property which "Gets or sets the line on which the type member statement occurs", it contains a FileName and LineNumber property to clearly identify where the statement is executed. So I thought to use it to instruct the CodeDom to add my method in the code file I wanted with the following code (and quite a few variations of it):

//Create an InitializeComponent2() method member = new CodeMemberMethod(); member.Name = "InitializeComponent2"; foreach (CodeTypeMember typememb in typedecl.Members) {     if (typememb.Name == "InitializeComponent")     {         member.LinePragma = new CodeLinePragma();         member.LinePragma.FileName = typememb.LinePragma.FileName;         break;     } } typedecl.Members.Add(member); //---------------------------------------------------------------------------

Despite my attempts, InitializeComponent2() is always created in Form1.cs... still something to look into smile_eyeroll.

Finally, the ugly workaround (or should I say "alternative solution"? smile_tongue) of adding and removing the fake label to the form to activate the changes made to the code; I guess this invokes a sort of refresh mechanism, so I tried to dig into those calls with Reflector because I wanted to find the property to set or the method to call to activate the refresh without adding the label, but again with no luck... third (and hopefully last) point to clarify.

Conclusion

CodeDom offers some interesting capabilities to control developers but also has some limitations, here's a list (likely not complete):

  • CodeCompile unit does not have space for using directives or namespace members, so they are placed now into first default namespace
  • using alias directive - no support found
  • nested namespaces - no support found (so parser is flattening namespace hierarchy)
  • variable declaration list (int i,j,k;) - no support - transformed to individual var declarations
  • pointer type - no support found
  • jagged array type (array of arrays) - CSharpCodeProvider reverses order of ranks
  • params keyword - not supported - param is omitted in parsing and param is then an ordinary array type param
  • private modifier on nested delegate is not shown by CSharpCodeProvider (all other nested types works fine)
  • unsafe modifier - no support found
  • readonly modifier - no support found
  • volatile modifier - no support found
  • explicit interface implementation - not implemented yet (I think this can be done)
  • add and remove accessors for Event - no support found
  • virtual and override modifiers do not work in CSharpCodeProvider for events
  • Operator members and Destructors - no support found
  • Expressions - no unary expressions(operations), only one dimension array, some operators not supported
  • Attribute targets - no support found
  • Attributes on accessor - no support found
  • If CompileUnit contains custom attributes in global scope, CSharpCodeProvider prints then before global using directives (because using has to be in the first namespace)

Back to the sample treated in this post, I'll try to resolve those 3 open problems I mentioned above, but if any of you have some details to add (or a solution to share smile_wink) please leave a comment, I'll be happy to correct the post! smile_nerd

 

Carlo

Quote of the day:
Addresses are given to us to conceal our whereabouts. - Saki

 
 

可从此处完成的操作:

 
 

没有评论:

发表评论