Blue Theme Orange Theme Green Theme Red Theme
 
Home | Forums | Videos | Photos | Downloads | Blogs | E-Books | Interviews | Jobs | Beginners | Training
 | Consulting  
Submit an Article Submit a Blog 
 Login Close
User Id:
Password:
 
Forgot Password
Forgot Username
Why Register
 Jump to
Skip Navigation Links
TechnologyExpand Technology
WebsiteExpand Website
 Resources  
Close
 Our Network  
Close
Search :       Advanced Search »
Home » General » Build your own Visual Studio: An Application Framework for Editing Objects at Run Time

Build your own Visual Studio: An Application Framework for Editing Objects at Run Time

This article describes a generic application framework that may be of some use in projects that would need an interface similar to Visual Studio. The application demonstrates approaches to providing a toolbox, a workspace, an object treeview, and an object editor.

Author Rank:
Technologies: .NET 1.0/1.1, Controls, Windows Forms,Visual C# .NET
Total downloads : 898
Total page views :  24073
Rating :
 3/5
This article has been rated :  4 times
   Print Read/Post comments Post a comment  Rate  
   Email to a friend  Bookmark  Similar Articles  Author's other articles  
Download Files:
ObjectManageCoder.zip
 
Become a Sponsor


Related EbooksTop Videos

Introduction: 

This article describes a generic application framework that may be of some use in projects that would need an interface similar to Visual Studio.  The application demonstrates approaches to providing a toolbox, a workspace, an object treeview, and an object editor. 

The application framework will allow the user to create objects in the workspace by dragging items out of the toolbox and onto the work surface, the user may drag the objects around within the workspace to reposition them, and the user may select objects in the workspace and use the property editor to change the object's properties.  Changes made in the property editor will immediately be reflected in the appearance of the object and in the treeview.  The treeview shows all of the workspace objects as well as child objects.  The user may also select objects from the treeview to push them into the property editor or to delete them from the project.  The user may save the workspace as a custom file type and use the application's file open command to restore the custom file back into the workspace for subsequent views and edits.

The application is not of any value in and of itself, it is just intended to demonstrate an approach to building a useful application that needs to incorporate these types of features.  The application is intended to serve only as a demonstration of one approach  to building this sort interface; it is not the only way to do it but it is a representative of a simple way to build a fairly powerful interface that will be familiar to many users through the use of similar products. 

The application is not complete in as much as a lot of typical functions are not described and mechanized in the project; for example, I have not addressed things like printing or object alignment and distribution.  It is intended to serve as the basis for a more complex application but it is not itself a complete or complex application.  You can alter the approach as I have described in any number of different ways and still achieve the same sort of resulting interface.  For example, I will demonstrate using buttons as the objects in the toolbox but in reality any object that works with drag and drop can be used in place of the buttons.

This application and article will attempt to demonstrate the following concepts:

  • Drag and Drop to support an application toolbox interface
  • Panel drag and drop to support moving existing objects at run time
  • Treeview control used to provide a map to all existing parent and child objects 
  • Treeview control used to allow object selection
  • Treeview control used to delete project objects and child objects through simple recursion
  • The property grid control used to edit object properties
  • Serialization used to store files containing all of the necessary data needed to reconstruct the file between uses (collective object persistence through a custom file type)
  • Deserialization used to recover stored, serialized objects to support a file open function and subsequently used to restore a stored workspace to the active application
  • Processing command line arguments to open a file immediately upon initialization
  • Creation of a custom file type through the setup and deployment package

Getting Started:

In order to get started, unzip the attachment and load the solution into Visual Studio 2005.  Examine the solution explorer and note the files contained in the project:

Figure 1:  The Solution Explorer Showing the Project Files

Beneath the solution title node, observe that there are two added folders, one containing graphics, and one containing classes.  The classes folder contains four classes; three are user controls and one is a standard class file.  These classes are used for demonstration purposes only and form the basis for the objects and object data used within the application.  The three user controls are:  Animal.cs, Person.cs, and Place.cs.  The remaining class (ObjectNote.cs) is the standard class.

The graphics folder contains a collection of icons and images used within the application.

In the solution root, note that there are two files:  FileSerialize.cs which is a module used to handle serialization and deserialization within the application.  The remaining class is the frmMain.cs; this class is the main application and it contains most of the code used to drive the application.

The main form (frmMain.cs) of the application has a layout consisting of a left side panel containing a tabbed panel; the tabbed panel has two tabs:  Toolbox and Properties.  The toolbox panel contains three user controls represented by buttons; these buttons can be dragged onto the form and whenever a control is dragged onto the form a control of the associated type is added to the workspace panel controls control collection as well as to a sorted list containing references to those objects added to the panel.  The properties tab is used to expose a treeview control and a property grid control.  The treeview control shows all objects contained within the current project; the property grid shows the properties for any object selected from either the workspace (panel control) or the treeview control.  The user may edit any property exposed but the application is only configured to persist the custom properties defined in the class; this could be modified to support persisting any or all of the values associated with the object but in an effort to keep the code small and readable, I opted only to support a few properties in serialization and deserialization.

Take a look at figures 2 and 3 to see an example of the demonstration application running; figure 2 shows the application with the toolbox tab selected; the objects in the toolbox panel may be dragged and dropped onto the panel in the right; in this example you can see several objects that have been dropped onto the panel.  Figure 3 shows the property tab selected in the application, the treeview at the top shows all of the objects currently in the workspace.  The property grid control is set to allow edits to be made to the last selected object.  Each time a new object is selected from the treeview or the panel, the property grid will update to show the properties for that selected object.

Figure 2:  Application Running with Toolbox Tab Selected

Figure 3: Application Running with Properties Tab Selected

The Code:  User Controls.

There are three user controls contained in the class folder in the solution.  They all are basically constructed the same way so I will only describe one of them and you can examine the others if you so desire; the user controls are arbitrary to this discussion; the application framework can be used to edit the properties of any object; the controls I have included are only useful for this demonstration.

Open up the Animal.cs class file.  This is user control; user controls can't be easily serialized so there is a little extra work involved to persist them and to bring them back to life between uses. 

Each of the user controls starts out with a few imports; these imports are used to add design support for the controls when in use.  The class starts with the following code:

using System.ComponentModel;

using System.ComponentModel.EditorAttribute;

using System.ComponentModel.Design.DesignerCollection;

 

public class Animal

After the class declaration, a region called declarations is defined and populated with private member variables and enumerations.  I have included the enumerations to demonstrate how the property grid displays options based upon an enumeration (they are exposed in a combo box within the property editor); the declaration region content is as follows:

#region "Declarations"

 

        private Guid mUniqueID;

        private string mDisplayName;

        private string mSpecies;

        private AnimalType mType;

        private HabitatType mHabitat;

        private System.Drawing.Color mColor;

        private Point mLocation;

        public enum AnimalType

        {

            Mammal,

            Amphibeon,

            Fish,

            Bird,

            Reptile

        }

        public enum HabitatType

        {

            Water,

            Desert,

            Forest,

            Arid,

            Mountain,

            Plain,

            Coastal

        }

 

#endregion

Following the declarations region, the "New" subroutine is overloaded; one version of the subroutine is used to create new instances of the control whilst the other is used to recreate the controls following deserialization:

//Default Constructor

        public Animal()

        {

            InitializeComponent();

            Guid newGuid = Guid.NewGuid;

            this.mUniqueID = newGuid;

            Habitat = HabitatType.Arid;

            Type = AnimalType.Mammal;

            DisplayName = "myAnimal";

        }

 

This is the default constructor used to create new instances of the control; it sets a unique ID for the control which is used in managing relationships within the treeview and it sets a few default properties for the control.  Next is the alternate constructor:

// Alternate constructor

        public Animal(Guid theId, string theDisplayName, string theSpecies, string theType, string theHabitat,     

        System.Drawing.Color theColor, Point theLocation)

        {

            InitializeComponent();

            // when restoring from data after deserialization, set the UID to

            // the stored value and the recovered properties for the control

            // including its location are stored into a new instance of the

            // user control

            mUniqueID = theId;

            DisplayName = theDisplayName;

            Species = theSpecies;

            Type = theType;

            Habitat = theHabitat;

            MajorColor = theColor;

            this.Location = theLocation;

        }

 

This is the alternate constructor used to restore a control following deserialization; the values supplied to the constructor will be supplied by a structure populated from the original control's properties at serialization.  Additional properties could easily be added however in effort to keep things simple I opted to only support a few of the available properties and only one from the user control class (location).  If you wanted to add in other properties such as a border style or background color, you could capture those values at serialization and post them back to the control whenever it is reconstructed through this alternate constructor.

The rest of the Animal class contains the properties used by the control.   When looking at the properties, examine each of the attributes assigned to the properties that may be edited within the property editor control.  That region is as follows:

#region "Properties"

 

        public Guid ID

        {

            get

            {

                return mUniqueID;

            }

        }

 

        public Point MyLocation

        {

            get

            {

                return mLocation;

            }

            set

            {

                mLocation = value;

            }

        }

        [CategoryAttribute("Animal Information"), Browsable(true), BindableAttribute(false), DescriptionAttribute

        ("The display name of this animal object.")]

        public string DisplayName

        {

            get

            {

                return mDisplayName;

            }

            set

            {

                mDisplayName = value;

                txtTitle.Text = "ANIMAL: " + mDisplayName;

            }

        }

 

        [CategoryAttribute("Animal Information"), Browsable(true), BindableAttribute(false), DescriptionAttribute  

        ("The species of this animal object.")]

        public string Species

        {

            get

            {

                return mSpecies;

            }

            set

            {

                mSpecies = value;

                lblSpecies.Text = "Species: " + mSpecies;

            }

        }

        [CategoryAttribute("Animal Information"), Browsable(true), BindableAttribute(false), DescriptionAttribute

        ("The type of this animal object.")]

        public AnimalType Type

        {

            get

            {

                return mType;

            }

            set

            {

                mType = value;

                lblType.Text = "Type: " + mType.ToString();

            }

        }

 

        [CategoryAttribute("Animal Information"), Browsable(true), BindableAttribute(false), DescriptionAttribute

        ("The habitat used by this animal object.")]

        public HabitatType Habitat

        {

            get

            {

                return mHabitat;

            }

            set

            {

                mHabitat = value;

                lblHabitat.Text = "Habitat: " + mHabitat.ToString();

            }

        }

 

        [CategoryAttribute("Animal Information"), Browsable(true), BindableAttribute(false), DescriptionAttribute

        ("The primary color of this animal object.")]

        public System.Drawing.Color MajorColor

        {

            get

            {

                return mColor;

            }

            set

            {

                mColor = value;

                lblColor.Text = "Color: " + mColor.ToString();

            }

        }

 

#endregion

 

As far as the attributes go, the Category Attribute is used to group properties within the property editor.  If three properties have a common Category Attribute, they will appear grouped together during edit.  The Browsable attribute determines whether or not the property will appear in the property editor.  The Description Attribute provides the text string that appears at the bottom of the editor; this is generally used to provide instructions to the user or to describe the property. 

 

Figure 4:  Property Grid in Use

That pretty much wraps up the content of the user controls; again all three are basically built the same and therefore I have described only one of the three user controls.

The Code:  Serializable Class.

The class folder contains one other class; it is called ObjectNote.cs and it is a simple, serializable class.  It is included in the demonstration for two reasons.  The first is that everything in a serializable class can be serialized (you probably guessed that was coming) and therefore you will not need to do anything extra to serialize, deserialize, or to reconstruct objects between uses.  The other reason I have included it in the demonstration is because I use it to show adding and removing child nodes from the treeview control.

The class is very simple and it only contains three properties worthy of note:  Its unique ID, its parent's unique ID, and string used to hold the note itself.  At runtime, if the user right clicks on an object in the treeview or in the panel, the application will reveal a context menu and one of the two menu options is to add a note to the object.  This is the container class that holds that note.

The code starts with a few imports and a class declaration.  Note that the class is marked serializable:

using System.ComponentModel;

using System.ComponentModel.EditorAttribute;

using System.ComponentModel.Design.DesignerCollection;

using System.Runtime.Serialization;

 

[Serializable]

public class ObjectNote

 

Following the imports and class declaration, a default constructor is defined as follows:

// Default Constructor

        public ObjectNote(Guid parent)

        {

            // In order to keep track of who this note belongs to when displayed

            // in the treeview control, we need to know the identity of the parent object (node)

            // Here we give the new object a new guid and set its parent property to the value passed to the

            constructor.

            Guid newGuid = Guid.NewGuid;

            this.mUniqueID = newGuid;

            this.mParentID = parent;

        }

 

As per the comments in the code, the constructor is passed in its parents unique ID (a guid) and its unique ID and parent ID properties are set.  The parent ID is used to do two things:  Maintain the relationship between objects stored in the treeview, and to allow child objects to be found and deleted if the primary node is deleted by the user.

Following the constructor, a few declarations are made within a region;  that section is as follows:

#region "Declarations"

 

        private Guid mUniqueID;

        private Guid mParentID;

        private string mDisplayName;

        private string mNote;

 

#endregion

These private member variables are set either when the control is created or by the properties which are added next:

#region "Properties"

 

        public Guid ID

        {

            get

            {

                return mUniqueID;

            }

        }

 

        public Guid MyParent

        {

            get

            {

                return mParentID;

            }

            set

            {

                mParentID = value;

            }

        }

 

        [CategoryAttribute("Object Note"), Browsable(true), BindableAttribute(false), DescriptionAttribute("Enter

        text to describe the associated object.")]

        public string Note

        {

            get

            {

                return mNote;

            }

            set

            {

                mNote = value;

            }

        }

 

        [CategoryAttribute("Object Note"), Browsable(true), BindableAttribute(false), DescriptionAttribute("The

        display name of this note object.")]

        public string DisplayName

        {

            get

            {

                return mDisplayName;

            }

            set

            {

                mDisplayName = value;

            }

        }

 

#endregion

 

Of the properties shown, the unique and parent IDs have already been mentioned.  The display name is the name of the note as it is displayed at run time and the Note property is the note assigned to the parent node.

That wraps up the controls used by the application.  Again, these classes were only defined to allow the demonstration of the approach defined within this demonstration; aside from the demonstration they don't serve any useful purpose (well, unless you have some unusual requirements and just so happen to need controls to monitor people, animals, and places).

The Code:  Main Form.

Main Form Layout.

The main form contains a menu at the top of the form, a tabbed control is docked to the left and has two panels, one is used as a toolbox and the other to show the treeview and properties.  The tabbed panel has a vertical splitter on its right side; within the property tab, that panel is split horizontally with the treeview docked to the top, the horizontal splitter under the treeview, and the property grid control set to dock full under the horizontal splitter.  On the right side of the splitter, a toolbar is docked to the top and a panel is set to dock full beneath the toolbar.  The panel is set to have a background color of white and it is used as the container for the objects.

Given the panel control is going to be the target for objects dragged from the toolbar, the Allow Drop property of the panel has to be set to true.

Each of the three user controls is represented by a button placed into the toolbox.  Each button has an image and a text label describing it.

The toolbar has two buttons added to it; one is called select and one is called drag or move.  If the user is in select mode, the clicking on an item in the panel will select it and make the control active for edit in the property grid; if the user is in drag mode, the mouse may be used to drag the objects around on the panel to reposition them.

In addition to the visual elements, the form also contains a file open dialog, a save file dialog, a context menu containing two options:  Delete Selected Element and Add Note To Selected Element.  There is also a tooltip and the main menu strip.

The main menu has a file menu with options to create a new file, open an existing file, save a file, and to exit the application.

The Main Form Code.

The main form's code is divided into several regions; these regions are as follows:  Declarations, Drag and Drop - Mouse Down Handlers, Drag and Drop - Drop Handlers, and Methods.

The declarations region contains a few member variables used within the application, that section is as follows:

#region "Declarations"

 

        //Private member variable declarations

        private string mCurrentObjectName;

        private object mCurrentObject;

        private SortedList mObjectCollection = new SortedList();

        private SortedList mObjectNotes = new SortedList();

        private SortedList mObjectFileBundle = new SortedList();

        //private bool mSelect = false;

        //Data Containers for Serialization --

        //This example uses three user controls for the basis of the

        //objects needed to demonstrate the concept; these are not

        //serializable and in order to persist them, you need to capture

        //the user controls properties into a serializable construct and

        //store that instead of the control itself.  The following

        //three structs are used to contain the user control data.

        //if you wish to persist other data from the control such as the

        //background color, font, etc., you would need to add additional

        //items to these data container structs and populate and recover

        //those values at runtime during the save and open file operations

        [Serializable()]

        private struct AnimalData

        {

            public Guid theId;

            public string theDisplayName;

            public string theSpecies;

            public string theType;

            public string theHabitat;

            public System.Drawing.Color theColor;

            public Point theLocation;

        }

        [Serializable()]

        private struct PersonData

        {

            public Guid theId;

            public string theDisplayName;

            public string thePersonName;

            public string theBirthPlace;

            public System.DateTime theDateOfBirth;

            public string theOccupation;

            public Point theLocation;

        }

        [Serializable()]

        private struct PlaceData

        {

            public Guid theId;

            public string theDisplayName;

            public string thePlaceName;

            public string theCity;

            public string theState;

            public string theCountry;

            public Point theLocation;

        }

 

#endregion

 

The private member variables are used to keep track of the currently selected object and the name of that object, there are two other collections (sorted lists) used to contain all of the user controls and all of the notes, an additional sorted list is used to pack up all of the object data and notes for serialization into a single file with a custom extension.

After the member variables have been declared, three serializable structures are defined: one for each of the three user controls.  Since we are not going to try to serialize everything in the user control, we are just adding in a few key properties.  As was mentioned earlier, it would be easy to add additional properties from the user control and to serialize and deserialize those properties between uses.

In the Drag and Drop - Mouse Down region, each of the three buttons is set up to support the drag and drop operation.  A handler was written for each button but since they are all basically the same, I will only show one here:

        private void Button1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)

        {

            //When the user clicks down on the button, we assume we are going to drag one to the

            //panel so we instance an animal, set its default properties and make the

            //DoDragDrop call.  The control is not yet added to the panel's control collection

            //and is not yet included in any data that will be serialized; the user has to

            //complete the drop onto the panel for this transaction to complete.

            Animal myAnimal = new Animal();

            mCurrentObjectName = "Animal";

            mCurrentObject = myAnimal;

            myAnimal.DisplayName = "myAnimal";

            Button1.DoDragDrop(myAnimal, DragDropEffects.All);

        }

 

The mouse down event is used to instance the associated class, set a couple of properties on the object, and evoke the button's Do Drag Drop method whilst passing it the new object and setting the Drag Drop Effect to all.  That sets up the drag side of the house, now we need to handle the drop side.  Looking at the drop handlers region, you will see two subroutines defined to prepare the panel control to receive the dropped items:  Panel1_DragDrop, and Panel1_DragEnter; they are written as follows:

 

        private void Panel1_DragDrop1(object sender, System.Windows.Forms.DragEventArgs e)

        {

            float dropX = e.X;

            float dropY = e.Y;

            object dropLocation = new Point(dropX, dropY);

            Point dropPoint = new Point();

            dropPoint = Panel1.PointToClient(dropLocation);

            if (mCurrentObjectName.ToString() == "Animal")

            {

                Animal myAnimal = new Animal();

                myAnimal = ((Animal)(this.mCurrentObject));

                Panel1.Controls.Add(myAnimal);

                myAnimal.Location = dropPoint;

                myAnimal.ContextMenuStrip = ContextMenuStrip1;

                mObjectCollection.Add(myAnimal.ID, myAnimal);

                myAnimal.MouseClick += /* might be wrong, please check */ new EventHandler(Object_MouseClick);

                myAnimal.MouseDown += /* might be wrong, please check */ new EventHandler

                (Object_MouseDown);

            }

            else if (mCurrentObjectName.ToString() == "Person")

            {

                Person myPerson = new Person();

                myPerson = ((Person)(this.mCurrentObject));

                Panel1.Controls.Add(myPerson);

                myPerson.Location = dropPoint;

                myPerson.ContextMenuStrip = ContextMenuStrip1;

                mObjectCollection.Add(myPerson.ID, myPerson);

                myPerson.MouseClick += /* might be wrong, please check */ new EventHandler

                (Object_MouseClick);

                myPerson.MouseDown += /* might be wrong, please check */ new EventHandler

                (Object_MouseDown);

            }

            else if (mCurrentObjectName.ToString() == "Place")

            {

                Place myPlace = new Place();

                myPlace = ((Place)(this.mCurrentObject));

                Panel1.Controls.Add(myPlace);

                myPlace.Location = dropPoint;

                myPlace.ContextMenuStrip = ContextMenuStrip1;

                mObjectCollection.Add(myPlace.ID, myPlace);

                myPlace.MouseClick += /* might be wrong, please check */ new EventHandler(Object_MouseClick);

                myPlace.MouseDown += /* might be wrong, please check */ new EventHandler

                (Object_MouseDown);

            }

            else

            {

                mCurrentObject.Location = dropPoint;

            }

            UpdateTreeview();

        }

 

The first part of this handler gets the current mouse position at the time of the drop and converts it into a value usable by the panel control.  This will make sure that the dropped item's upper left hand corner will be located at the exact position indicated by the mouse when the drop was made.  Following the location conversion, the subroutine evaluates the current object's name (set when the button received its mouse down command) within a select case statement; depending upon the current object, the subroutine then creates an instance of the appropriate object type sets it to be the current object, adds the control to the panel, sets its location to be at the drop point, and then adds a context menu and mouse click and mouse down event handlers to the control .

Once the object has been placed and added to the panel, the treeview control is updated to show the newly added object.  The Drag Enter event handler is set up to handle whether or not the drag drop will be used to move a control or add a new one.

        private void Panel1_DragEnter1(object sender, System.Windows.Forms.DragEventArgs e)

        {

            if ((e.Data.GetDataPresent(DataFormats.Text)))

            {

                e.Effect = DragDropEffects.Copy;

            }

            else

            {

                e.Effect = DragDropEffects.Move;

            }

        }

 

The last section of code to describe is contained in the Methods region; within this region, fifteen separate subroutines are defined.  Some of these subroutines are very simple and are barely worth mentioning, others will require some explanation.

The first two subroutines in the methods region are two generic handlers:  a mouse down handler, and an mouse click handler.

        private void Object_MouseClick(object sender, System.Windows.Forms.MouseEventArgs e)

        {

            //A generic mouse click handler used for dynamically added user control objects.  This sets the current

            object, current object

            //name, and object associated with the property grid in response to a click

            try

            {

                this.mCurrentObject = sender;

                this.mCurrentObjectName = sender.DisplayName.ToString();

                this.PropertyGrid1.SelectedObject = sender;

            }

            catch (Exception ex)

            {

            }

        }

 

        private void Object_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)

        {

            //A generic mouse down handler used for dynamically added user control objects.

            //This works such that if the user to swap between selecting a control to load

            //it into the property grid control, or to allow the user to drag a control to a new location on the form

              panel.

            try

            {

                if (mSelect == true)

                {

                    mCurrentObject = sender;

                    mCurrentObjectName = sender.DisplayName.ToString();

                    mCurrentObject.DoDragDrop(mCurrentObject, DragDropEffects.All);

                    this.PropertyGrid1.SelectedObject = sender;

                }

            }

            catch (Exception ex)

            {

            }

        }

 

The comments provided within each of these two subroutines defines the purpose of the handler.  In general, these handlers are added to new instances of any controls added to the panel control at run time.

The next two subroutines are used to set a Boolean value used to allow the application to determine whether or not it is in select or drag mode.  This code is the click event handler for the two toolbar controls.  That code is as follows:

        private void tbnSelect_Click_1(object sender, System.EventArgs e)

        {

            //Select mode is used allow the user to click on an object without dragging

            mSelect = false;

        }

 

        private void tbnDrag_Click_1(object sender, System.EventArgs e)

        {

            //With select mode disabled the user can drag objects around to move them

            mSelect = true;

        }

 

Moving right along, the next thing up is the call that updates the treeview.  This call is made whenever a property grid item is edited or an item is added or deleted from the current workspace.  That code looks like this:

 

        private void UpdateTreeview()