Jump to content
Xtreme .Net Talk

Recommended Posts

Posted

I have a TextBox control where instead of specifying BackColor, Font, ForeColor, you can specify a list of named styles in the control. Then you can select by name which one of the styles to use. It’s a pretty useless feature, but this is only a test project to work out some designer issues for a more complicated control I’m working on.

 

My problem is that when I add styles to the control at design time (using a designer verb and custom entry form), they are not being serialized.

 

I have followed Devil’s excellent tutorial on Designers for Collection Controls. However, his sample has collections of components. I don’t have any components, I simply have a collection of objects. Therefore, I don’t think I can use the IDesignerHost services discussed in his tutorial. Do you need to use IDesignerHost services to tell the IDE to serialize your control, or only if you are using components?

 

Below are the highlights of this control. The full code is attached as a ZIP.

 

Hope someone has some ideas or can point me in a new direction.

Thanks.

 

[Designer(typeof(CustomTextDesigner))]
public class CustomText : System.Windows.Forms.TextBox
{
 private TextDataCollection theItems;
 private string backStyle;

 // Stuff and more stuff here

 [browsable(false), 
 DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
 public TextDataCollection TextDataCollection
 {
   get { return theItems; }
 }
 public string BackStyle
 {
   get { return backStyle; }
   set 
   { 
     backStyle = value; 
     try
     {
       base.BackColor = theItems.GetDataByName(value).BackColor;
     }
     catch
     {
       base.BackColor = Color.Violet;
       // I just want to see when this fails.
     }
   }
 }
}

internal class CustomTextDesigner : System.Windows.Forms.Design.ControlDesigner
{
 CustomText MyControl;  // Assigned to component in the Initialize method

 // Stuff and more stuff here.

 // This is the event handler to my designer verb!
 private void ConfigureItemStylesEventHandler(object sender, System.EventArgs e)
 {
   IDesignerHost h;
   DesignerTransaction dt;
   IComponentChangeService c;
   TextData newItem;

//    // This is Devil's code in his demo project
//    dt = h.CreateTransaction("Add Button");
//    button = (ColourButton) h.CreateComponent(typeof(ColourButton));
//    c.OnComponentChanging(MyControl, null);
//    MyControl.Buttons.Add(button);
//    c.OnComponentChanged(MyControl, null, null, null);
//    dt.Commit();


   // This is my attempt to implement Devil’s code to my uses.
   h = (IDesignerHost) GetService(typeof(IDesignerHost));
   c = (IComponentChangeService) GetService(typeof(IComponentChangeService));

   dt = h.CreateTransaction("Add TextStyle");
   newItem = (TextData) h.CreateComponent(typof(TextData));
   // Trouble here. I get the compile error:
   // 'SerializationTest.TextData' denotes a 'class' where a 'variable' was expected
   // I suspect this is because TextData does not inherit from Component.
   // What do I do?

   // Another problem is that TextData takes two parameters in its constructor.
   // One is a string that is the object’s name, and one is a Color that is
   // the object’s color. How do I use the services to create an object with
   // constructor parameters?


//    // This is my original code. This did not serialize.
//    newItem = new TextData("Red", Color.Red);
//    MyControl.TextDataCollection.Add(newItem);
//
//    newItem = new TextData("Blue", Color.Blue);
//    MyControl.TextDataCollection.Add(newItem);
//
//    newItem = new TextData("White", Color.White);
//    MyControl.TextDataCollection.Add(newItem);

 }
}

[TypeConverter(typeof(TextDataTypeConverter))]
public class TextData
{
 private string name = "";
 private Color backColor = Color.White;

 public TextData(string Name, Color color)
 {
   name = Name;
   backColor = color;
 }

 // Name and Color properties here.

}


public class TextDataCollection : CollectionBase
{
 // This is a simple collection class to hold the 
 // collection of TextData objects. The implementation
 // is not shown here.
}


internal class TextDataTypeConverter : TypeConverter
{
 public override bool CanConvertTo(ITypeDescriptorContext context, Type destType)
 {
   if (destType == typeof(InstanceDescriptor))
     return true;

   return base.CanConvertTo(context, destType);
 }

 public override object ConvertTo(ITypeDescriptorContext context, 
   System.Globalization.CultureInfo culture, object value, Type destType)
 {
   if (destType == typeof(InstanceDescriptor))
   {
     ConstructorInfo ci = typeof(TextData).GetConstructor( 
       new Type[]{typeof(string), typeof(Color)} );

     TextData td = (TextData)value;
     return new InstanceDescriptor(ci, new object[]{td.Name, td.BackColor});
   }

   return base.ConvertTo(context, culture, value, destType);
 }
}

SerializationTest.zip

Posted

Alright, that was a bit overboard. Let me simplify this a bit if I can.

 

I have a control where one of its properties is a collection of objects that I need to have serialized:

[browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public TextDataCollection TextDataCollection
{
 get { return theItems; }
 // where theItems is a collection of my TextData objects.
}

Each TextData object in this collection is defined fully by the constructor


public TextData(string Name, Color color)
{
name = Name;
backColor = color;
}
[/Code]

I also have a type converter for the TextData object.

 

I thought this was all I needed to have the data serialized so that it can be persisted, but it is not serializing. Are there other steps I need to do?

 

Thanks.

  • *Gurus*
Posted

Ok, I managed to get your serializer working. You're right, you don't need nearly as much stuff in your ConfigureItemStylesEventHandler method as I had in mine as you're not dealing with components. The bare minimum you need to do is create your object and add it to the collection, no DesignerHost is required, you just create it using the normal constructor as it's not sited.

 

You'll want to do a little more than the bare minimum though, and let the IComponentChangeService know what you're doing so undo and redo will work:

 

		c = (IComponentChangeService) GetService(typeof(IComponentChangeService));
		newItem = new TextData("test", Color.Red); //(TextData) h.CreateComponent(typof(TextData));
		c.OnComponentChanging(MyControl, TypeDescriptor.GetProperties(MyControl)["TextDataCollection"]);
		MyControl.TextDataCollection.Add(newItem);
		c.OnComponentChanged(MyControl, TypeDescriptor.GetProperties(MyControl)["TextDataCollection"], null, null);

 

You were also opening a DesignerTransaction and not closing it, which will completely screw the designer up.

 

That's all I needed to do to get your collection serializing. Bear in mind you can gain a lot by implementing an AddRange method on your collection, which the designer will use instead of repeated Add calls if it's available.

MVP, Visual Developer - .NET

 

Now you see why evil will always triumph - because good is dumb.

 

My free .NET Windows Forms Controls and Articles

Posted

Ah! Excelent! Thanks Devil! I never would have gotten here without your help via this forum and your tutorials.

 

If I add an AddRange method to the collection class, and then add repeated new styles to the control, will the designer automatically use the AddRange method instead of repeated Add calls, or do I need to use AddRange myself, or do something else special, when I add the new styles?

 

I had to add a DesignerTransaction to enable the undo/redo, and I was getting an error when the designer tried to serialize the normally read/write BackColor property. So I've made the following tweaks:

 


// In my CustomText Control class:
[browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Color BackColor
{
get { return base.BackColor; }
}

// The event handler for the designer verb in the ControlDesigner
private void ConfigureItemStylesEventHandler(object Sender, System.EventArgs e)
{
// Declare some objects
IDesignerHost h;
DesignerTransaction dt;
IComponentChangeService c;
TextData newItem;

// Get some services we'll need
h = (IDesignerHost) GetService(typeof(IDesignerHost));

// Initialize a designer transaction. This will start 'recording' our actions
// so that the designer can undo them if we want.
dt = h.CreateTransaction("Add TextStyle");

// Create the new item(s)
c = (IComponentChangeService) GetService(typeof(IComponentChangeService));
newItem = new TextData("test", Color.Red);
c.OnComponentChanging(MyControl, TypeDescriptor.GetProperties(MyControl)["TextDataCollection"]);
MyControl.TextDataCollection.Add(newItem);
c.OnComponentChanged(MyControl, TypeDescriptor.GetProperties(MyControl)["TextDataCollection"], null, null);

// Finally, commit the changes to the designer transaction. This
// will stop the dt from recording our actions.
dt.commit();

// We're done.
}
[/Code]

Thanks for all your help.

Posted

Okay, Devil, I've got another question:

 

Suppose that the objects being serialized cannot be fully described by their constructor. For example, say I add a new property to the TextData object for a ForeColor, that is not included in the constructor. Then creating new text styles would look something like:


 // Create the new item(s)
 c = (IComponentChangeService) GetService(typeof(IComponentChangeService));
 newItem = new TextData("test", Color.Red);
 newItem.ForeColor = Color.DeepSkyBlue;
 c.OnComponentChanging(MyControl, TypeDescriptor.GetProperties(MyControl)["TextDataCollection"]);
 MyControl.TextDataCollection.Add(newItem);
 c.OnComponentChanged(MyControl, TypeDescriptor.GetProperties(MyControl)["TextDataCollection"], null, null);

 

But when I try this, the "Test", Color.Red is serialized, but not the ForeColor property. Is this something that needs to be addressed in the TypeConverter for the TextData class? I'm still searching MSDN for this but I haven't found the answer yet. Can you point me in the right direction?

 

Thanks again.

  • *Gurus*
Posted

Pass false instead of true when you create your InstanceDescriptor, and a local variable will be declared in InitializeComponent to configure the object before it is added.

 

I highly recommend providing more constructors to handle this though, as I have found local variables in InitializeComponent to be unreliable.

MVP, Visual Developer - .NET

 

Now you see why evil will always triumph - because good is dumb.

 

My free .NET Windows Forms Controls and Articles

Posted

Thanks again. I'm back to working on my real project as opposed to the test application, and I think it will be difficult to put all the item data into the constructor, but I'll take a good look at it.

 

I'm still having some problems - I'm getting a null reference error - that I can't track down. Man! Debugging the designer is a PITA! I've seen some posts on how to start a second VS.NET instance and use it to debug the designer running in the first instance. I've tried:

 

1) Put your control on a form in VS.NET

2) Start a 2nd Vs.Net

3) Choose the Debug menu >> Processes ...

4) Double click "devenv.exe" and choose "Common Language Runtime" as the types of debugging

5) Open your code file, set your breakpoint, and you're debugging.

 

When I try this, the 2nd instance is essentially blank. I set my breakpoint in the 1st instance, but the code never stops. I know the code is making it to the break points because I have MessageBox objects here and there letting me know what is going on.

 

Is it possible to use the debugger for a control designer?

  • *Gurus*
Posted
You have to open the code files and set breakpoints in the debugger, i.e. the second instance of Visual Studio. Go to Debug -> Exceptions and tell the debugger to break the first time any exception is thrown, that's the easiest way of stepping through code that falls over in the first instance.

MVP, Visual Developer - .NET

 

Now you see why evil will always triumph - because good is dumb.

 

My free .NET Windows Forms Controls and Articles

Posted

Got the debugger working, thanks. But I still can't solve my problem. I�m back to my test application, which is attached below. My control now has a collection of collections. Yup, the user control saves a collection of style objects. Each style object in turn has a sub collection of other style objects. I know the implementation in the test control is a useless feature, but it makes sense in my real control, trust me.

 

The first collection is being saved nicely, and any straight properties of the objects in the first collection are also being saved. However, the sub collection is not being serialized, and none of the sub objects are being serialized. I'm getting the error:

 

"Code generation for property 'SubItem' failed. Error was: 'Object reference not set to an instance of an object.'" (SubItems, by the way, is the property name for the second collection.)

 

Both collection items have a type converter, and both collection properties are set to serialize content. Is there anything else I need to do? Do I need two ControlDesigners maybe? Any thoughts?

 

Just had a thought: Do I need to do a separate

PropertyDescriptor PropImChanging = TypeDescriptor.GetProperties(MyControl)["SubItems"];

c.OnComponentChanging(MyControl, PropImChanging);

for the sub collections? In other words, will Serialize.Content only go one deep? If so, how would I sequence this?

 


[TypeConverter(typeof(TextDataTypeConverter))]
public class TextData
{
// Stuff

public TextData(string Name, Color color) {...}
public string Name {...}
public Color BackColor {...}
public Color ForeColor {...}

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public TextSubCollection SubItems
{
get { return subItems; }
}
}


[TypeConverter(typeof(SubItemTypeConverter))]
public class SubItem
{
private string myValue = "";
public SubItem(string MyValue)
{
myValue = MyValue;
}
public string MyValue
{
get { return myValue; }
set { myValue = value; }
}
}


public class TextDataCollection : CollectionBase
{
public TextData this[int index] {...}
public int Add(TextData item) {...}
public TextData GetDataByName(string Name) {...}
}


public class TextSubCollection : CollectionBase
{
public SubItem this[int index] {...}
public int Add(SubItem item) {...}
}


internal class TextDataTypeConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context, Type destType)
{
// Boiler Plate
}

public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destType)
{
if (destType == typeof(InstanceDescriptor))
{
ConstructorInfo ci = typeof(TextData).GetConstructor(
new Type[]{typeof(string), typeof(Color)} );

TextData td = (TextData)value;
return new InstanceDescriptor(ci, new object[]{td.Name, td.BackColor}, false);
}

return base.ConvertTo(context, culture, value, destType);
}
}


internal class SubItemTypeConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context, Type destType)
{
// Boiler Plate
}

public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destType)
{
if (destType == typeof(InstanceDescriptor))
{
ConstructorInfo ci = typeof(TextData).GetConstructor(
new Type[]{typeof(string)} );

SubItem si = (SubItem)value;
return new InstanceDescriptor(ci, new object[]{si.MyValue}, false);
}

return base.ConvertTo(context, culture, value, destType);
}
}


// From inside the handler for the designer verb:
// Add a new text style
h = (IDesignerHost) GetService(typeof(IDesignerHost));

newItem = new TextData("Blue", Color.Blue);
newItem.ForeColor = Color.Gold;
subItem = new SubItem("Blue One");
newItem.SubItems.Add(subItem);
subItem = new SubItem("Blue Two");
newItem.SubItems.Add(subItem);

dt = h.CreateTransaction("Add TextStyle");
c = (IComponentChangeService) GetService(typeof(IComponentChangeService));

PropertyDescriptor PropImChanging = TypeDescriptor.GetProperties(MyControl)["TextDataCollection"];

c.OnComponentChanging(MyControl, PropImChanging);

// Add a new property
MyControl.TextDataCollection.Add(newItem);

c.OnComponentChanged(MyControl, TypeDescriptor.GetProperties(MyControl)["TextDataCollection"], null, null);
dt.Commit();

[/Code]

SerializationTest.zip

Posted
Well Fudge! This is where I need a smiley of Homer Simpson going D'oh!That was it, fixed the return type and all is well. I've been staring at this for two days now and just didn't see it. Thanks.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...