Debugging Design-Time Controls
Steve Lasker
Immedient
November 2002
Applies to:
Microsoft® Visual Studio® .NET
Microsoft Windows® Forms
Microsoft® ASP.NET
Microsoft® Mobile Internet Toolkit
Microsoft® .NET Compact Framework
Summary: Walks through the process of debugging any custom design-time control when it is being used with the Microsoft Visual Studio .NET designer, and describes the chain of events that take place when you work with controls in the design surface. (13 printed pages)
Download Designtimedebugging.exe.
Introduction
Setting Up the Environment
Creating the Blank Solution
Creating a Control Library
Creating the Windows Forms Host Project
Starting Our Debugging Session
Chain of Events
Summary
An important part of developing any design-time control is the ability to troubleshoot and debug your code. When working with a control or Microsoft® Windows® Form in the design-time environment of Microsoft Visual Studio® .NET, your code is being executed even though the project does not appear to be running. This can pose a challenge as far as debugging goes. If you are creating any type of object that will have a design-time view, this article shows you how you can catch errors and debug when your object is being used within the Visual Studio designer. I will also describe some of the chain of events that happen when you work with controls in the design surface. The best part is that you don't have to create any wrapper objects. The Visual Studio .NET team baked the debugging environment right in.
To show the steps of troubleshooting and debugging your code as you change values or drop controls onto the design surface, we'll create an example base Windows Form that has a string property. When we change the value of the property, we'll step into the debugger.
In order to minimize the potential for errors due to lack of specifics, all files and directories have specific names. You're more then welcome to change these as you'd like. However, for clarity and ease of demonstration, I'll specify everything in detail.
The first step is to create an environment to host our code. We'll create:
- A blank solution to host our projects.
- A control library to host our compiled control.
- A Windows Forms project to host our custom control.
On the File menu, click New, and then click Blank Solution.
Name the solution DesignTimeDebugging.
Place the solution in c:\ DesignTimeDebugging.
Figure 1. Creating a blank solution to contain several projects
Since our purpose is to demonstrate debugging, not to create custom controls, we won't worry about creating an elaborate control. We'll just create a simple Form with a bogus property.
On the File menu, click New, and then click Project.
In the left window, click either Visual Basic Projects or Visual C# Projects.
In the right window, click Windows Control Library.
Name the control library Immedient.Windows.Forms. Notice how we follow the same convention as Microsoft. The only difference is that instead of "System" we're using "Immedient." This identifies a unique namespace, yet with consistency.
Delete UserControl1.
Figure 2. Adding a control library for your design-time control
Right-click the Control Library project. Click Add, and then click Add Windows Form.
Name the Form "Form."
Note We're purposely naming the Form with a collision to demonstrate namespaces.
Add the following code to the body of the class definition:
Visual Basic .NET Private _myText As String ''' <summary> ''' A Custom property used for this silly sample ''' </summary> ''' <value>A value of no meaning</value> <Description("A Custom property used for this silly sample"), _ DefaultValue("Hello"), _ Category("Appearance")> _ Public Property MyText() As String Get Return _myText End Get Set(ByVal Value As String) If (_myText <> Value) Then _myText = Value End If End Set End Property C# private string _myText = "Hello"; /// <summary> /// A Custom property used for this silly sample /// </summary> /// <value>A value of no meaning</value> [ Description("A Custom property used for this silly sample"), DefaultValue("Hello"), Category("Appearance") ] public string MyText { get{return _myText;} set { if(_myText != value) { _myText = value; } } }
Private _myText As String = "Hello";
This is a private variable to hold the contents of our public property.
[ ] in C# or <> in Visual Basic .NET
In C#, all code between the square brackets (<> in Microsoft® Visual Basic® .NET) are attributes. These inform the common language runtime (CLR) to behave in a given way once constructed. Attributes are sort of a cross between a constructor and pre-processor directive. They're set at coding time, and can't be changed at run time.
Description("A Custom property used for this silly sample") This
adds a description to the bottom of the property sheet for your property. We usually duplicate the same text from our XML summary documentation. (Hopefully the product team will merge these two declarations in the future.)
Figure 3. Your custom property, with its description, displayed on the property sheet
DefaultValue("Hello")
This is used in combination with the default value of our private variable. If the value of the property doesn't match the value in the DefaultValue attribute, the property sheet will show the value in bold. Visual Studio .NET will also generate code in the InitializeComponent() method to set the value. If the property sheet value matches the value in the DefaultValue attribute, the property sheet value will not be displayed in bold, and the InitializeComponent() will not have any reference to the property. This is great for inheritance. If none of the derived objects changed this property, they will inherit any changes you make to the base class. This can be a very powerful feature when used properly.
Category("Appearance")
This value is used to sort the properties in the property sheet.
- On the File menu, click New, and then click Project.
- In the left window, click either Visual Basic Projects or Visual C# Projects.
- In the right window, click Windows Application. (This will host our sample.)
- Name the project DesignTimeDebugging.
- Be sure to verify that the radio button is set to Add to Solution
Within our Host App Project, right-click References, and click Add Reference.
On the Projects tab, select our Immedient.Windows.Forms project.
Figure 4. Adding a project reference to your design-time control
Recompile the project to provide Microsoft IntelliSense®.
Change the inheritance of Form1 by changing the following code, in bold, to inherit from the Form we just created.
Visual Basic .NET Public Class Form1 Inherits Immedient.Windows.Forms.FormC# namespace Immedient.Samples.HostApp.Windows { public class Form1 : Immedient.Windows.Forms.Form {
Open the Form1, and notice the custom property on the Property Sheet. If you receive errors, close all open files, click Rebuild Solution, and then open the Form again in design mode.
There are a couple of properties you'll want to set, and one to verify on our control library project.
You'll want to confirm that you're actually generating the required debugging information for Visual Studio .NET to step into your code. If you get the "?" symbol on your breakpoints when you start the debugging session, you're not generating the appropriate symbol files. This may be because you're in release mode, or because the setting was changed accidentally. Be sure the Generate Debugging Information property is set to true.
The following settings will only apply to C# projects. In Visual Studio .NET 2002, Visual Basic .NET does not have a means to generate XML documentation.
In order to benefit from the C# XML documentation we created, you need to tell Visual Studio .NET to generate an XML file. In the XML Documentation File property, set the value to the same name as your assembly with the xml extension. (It would be nice if they just had a checkbox for this, but oh well...)
Finally, there's one more trick about XML documentation. Once you set the value for XML Documentation File, Visual Studio .NET is very helpful in warning you of every public interface that doesn't have documentation assigned. This gets real annoying, real fast. So, if you're not ready to deal with all the public interface documentation, you can set the Warning Level to 2. The task list won't be cluttered with warnings until you're ready to address them by filling in the documentation.
Figure 5. Setting properties related to XML documentation in a C# project
Now that we have some code to debug, let's start the session. In order to debug our code, we need to step into the application that is hosting our code. In this case it's Visual Studio .NET.
Note Visual Studio .NET was designed, just like the rest of the .NET Framework, from the ground up to host the design environment. Remember, the product team used the same set of tools we use. All we need to do is to start our class library within Visual Studio .NET. We do this with some properties on the Project.
Within the Debugging section, there are a couple of properties we need to change. They are slightly different in Visual Basic .NET and C#:
Right-click on the Control Library project and click Properties.
Click Configuration Properties and select the Debugging sub option.
Change the Start Action to Start External Program:, and enter the path of the Visual Studio .NET IDE: C:\Program Files\Microsoft Visual Studio .NET\Common7\IDE\devenv.exe.
Figure 6. Setting the startup program to host your debugging session (Visual Basic .NET)
Right-click on the Control Library project and click Properties.
Click Configuration Properties.
Change the Debug Mode to Program.
Change the Start Application to Visual Studio .NET. The default location is: C:\Program Files\Microsoft Visual Studio .NET\Common7\IDE\devenv.exe.
Figure 7. Setting the startup program to host your debugging session (C#)
Set a breakpoint on theif statement
within the set of the property.
Note You won't want to set a breakpoint on the get without setting an assert, as the design-time environment will "get" the value many more times than you want to hit F5.
Figure 8. Setting the breakpoint in your custom property
Be sure the Control Library project is the Startup Project. Set this by right-clicking the Control Library project and then click Set as Startup Project.
- Start the debugging session by hitting F5. Notice how a new instance of Visual Studio .NET has launched. Your development environment is hosting Visual Studio .NET within it—pretty cool.
- An easy way to tell which instance of Visual Studio .NET is your hosting debug environment is to notice the debugging buttons; it's the one with the start button grayed out.
- Once Visual Studio .NET starts up again, open the same solution: C:\DesignTimeDebugging\DesignTimeDebugging.sln.
- Within the HostApp Windows Forms project, open Form1 by double-clicking Form1 in the Solution Explorer.
- Within the property sheet, change the value of the MyText property to
Good Bye
. - You should step into your code.
- You have just debugged your first .NET design-time control. Let's take a look at the chain of events that happen in the background.
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 .NET. 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 .NET. The only thing magic about this method is its name. Visual Studio .NET just knows to look for this method.
In Visual Basic .NET, if Visual Studio .NET 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 .NET 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 .NET, 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 .NET 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 .NET creates your object as it parses the InitializeComponent() method. Once the object is constructed, Visual Studio .NET keeps track of the objects it creates, and simply says:
newlyCreatedObject.DesignMode = true
To have some fun and see what events fire, and when the DesignMode property is set, drop the following code in your Immedient.Windows.Forms.Form. In order to get the events to fire in Visual Studio .NET, you'll need to close all the forms, and then recompile your solution.
Visual Basic .NET
Private Sub Form_Layout(ByVal sender As Object, _
ByVal e As System.Windows.Forms.LayoutEventArgs) _
Handles MyBase.Layout
MessageBox.Show("layout: DesignMode=" + _
Me.DesignMode.ToString())
End Sub
Private Sub Form_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
If Me.DesignMode Then
' Don't connect to the database at design time
MessageBox.Show("Form Load: DesignMode=" + _
Me.DesignMode.ToString())
Else
' Make a connection to the database, and do something
MessageBox.Show("Form Load: DesignMode=" + _
Me.DesignMode.ToString())
End If
End Sub
Private Sub Form_VisibleChanged(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles MyBase.VisibleChanged
MessageBox.Show("VisibleChanged: DesignMode=" + _
Me.DesignMode.ToString())
End Sub
Private Sub Form_Paint(ByVal sender As Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) _
Handles MyBase.Paint
MessageBox.Show("Paint: DesignMode=" + _
Me.DesignMode.ToString())
End Sub
In this article, we used a simple property on a Windows Form. However, anything that changes in the design-time environment can be debugged. I originally found this technique when developing a Microsoft® ASP.NET Image HREF design-time control. This same technique applies to component controls, like the EventLog and Data controls that display in the system tray as well. Visual Studio .NET provides a rich, productive development environment for developing controls, in which it's just as easy to create and debug controls as it is to consume them.
Steve Lasker is the National Director of Research & Development at Immedient. Steve has been developing .NET controls and frameworks for Windows Forms, ASP.NET, and the .NET Compact Framework. Steve can be reached at Steve.Lasker@Immedient.com.