1

I wrote my own UITypeEditor and I reached my goal with help of @Sefe and with THIS link.

My basic setup: My basic setup

In this setup, BaseForm extends System.Windows.Forms.Form. Here i'm putting my property (List<Control> ActionButtons) that have a custom UITypeEditor like a modal style.

Resumed workflow (All here are in Design Time):

    1 - Open MyForm.
    2 - Click at ActionButtons(inherited by BaseForm) ellipsis [...] on MyForm properties panel.
    3 - A custom form is opened with inflated objects that I want pick. (this form is called by my CustomUITypeEditor)
    4 - Objects that I want are picked, and I close the form. So now, the data are ok in MyForm and serialized into Designer.cs file. I can reopen that EditorUI clicking again in ellipsis and see objects that I picked before.
    5 - Now when I close MyForm and reopen it, all data are lost!!! But the data still serialized into Designer.cs.     6 - If I do all this steps with a string instead List<Control>, all are Ok.

Bellow my code:

public class CollectionTypeEditor : UITypeEditor {

private IWindowsFormsEditorService _editorService = null;
private ICollection<Control> mControls = null;
private List<Control> mPickedControls = null;

// Editor like Modal style
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) {
  return UITypeEditorEditStyle.Modal;
}

// Opens modal and get returned data
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) {
  if (provider == null)
    return value;

  _editorService = (IWindowsFormsEditorService) provider
    .GetService(typeof(IWindowsFormsEditorService));

  if (_editorService == null)
    return value;

  mControls = new List<Control>();

  // retrieve old data
  mPickedControls = value as List<Control>;
  if (mPickedControls == null)
    mPickedControls = new List<Control>();

  // getting existent controls that will be inflated in modal
  Control mContext = (Control) context.Instance;
  GetControls(mContext);

  // open form and get response
  CollectionDesign<Control> frmCollections = new CollectionDesign<Control>(mControls, ref mPickedControls);
  var response = _editorService.ShowDialog(frmCollections);

  // returning data from editor
  return response == DialogResult.OK ? mPickedControls : value;
}

Everything works well here. Code for my variable in BaseForm. Ps.: this variable will be showed on MyForm where I make click at ellipsis [...]

[Editor(typeof(CollectionTypeEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(ActionButtonConverter))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public List<Control> ActionButtons { get; set; }

The serialization attribute was added because the file couldn't be saved. When form is closed and reopened, all data are lost.

The stranger thing is that I wrote other UITypeEditor like the same way, just changing type of data to string and I can close or reopen my form and all works fine, the data are saved.

I already added a TypeConverter but I think that isn't case here. what is wrong with my code?

I'm getting this error on reopen form:

Severity Code Description Project File Line Suppression State Message Method 'System.CodeDom.CodePropertyReferenceExpression.Add' not found.        

UPDATE

Now my List of controls are stored in myForm.designer file when it's closed or reopened, but the controls don't are attached on property grid. i.e.: If I click on ellipsis to add a Button 'addbt' when close and reopen myForm the code is auto generated.

Code auto generated bellow on myForm.designer:

  // 
  // addbt
  // 
  this.addbt.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
  this.addbt.Cursor = System.Windows.Forms.Cursors.Hand;
  this.addbt.Font = new System.Drawing.Font("Tahoma", 9F);
  this.addbt.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(222)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))));
  this.addbt.Location = new System.Drawing.Point(13, 6);
  this.addbt.Name = "addbt";
  this.addbt.Size = new System.Drawing.Size(103, 33);
  this.addbt.TabIndex = 0;
  this.addbt.Text = "Incluir";
  this.addbt.UseVisualStyleBackColor = true;

  // 
  // myForm
  // 
  this.ActionButtons.Add(this.addbt);

If I make a click on ellipsis again, the data are attached to property on myForm's PropertyGrid. But when I reopen myForm, this values stored before aren't passed to property on myForm's PropertyGrid (data still stored in auto generated code designer). So when a click on the ellipsis [...] the value from EditValue method not comes with data stored. I'm feeling that it's closer :)

Maybe something is wrong with my TypeConverter below:

public class ActionButtonConverter : TypeConverter {

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) {
  if (sourceType == (typeof(string)))
    return true;

  return base.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) {
  if(value.GetType() == typeof(string)) {
    List<Control> ctrs = value as List<Control>;

    if (ctrs != null)
      return ctrs;
  }

  return base.ConvertFrom(context, culture, value);
}


public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) {
  if (destinationType == typeof(string))
    return true;
  return base.CanConvertTo(context, destinationType);
}

public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) {
  if (value == null)
    return "null";

  if (destinationType == typeof(string))
    return "(custom collection)";

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

I think that I have anything error in deserialization. When myForm is reopened the ITypeDescriptorContext or value (TypeConverter) don't have anything about serialized data into designer.cs file.

Thanks all, and sorry for bad language :P

Community
  • 1
  • 1
Henrique
  • 198
  • 5
  • 11
  • I don't quite understand what's happening. When you reopen the form, do what do you see in the property grid? And when you click the ellipsis button, is your form designer populated or empty? – Sefe Dec 02 '16 at 18:01
  • @Sefe When I open the form the variable in property grid is null, but the `NameForm.Designer.cs` file has hold correctly data like I showed in UPDATE section. That is a Serialization problem or a TypeConverter problem? – Henrique Dec 02 '16 at 18:27
  • When you open your editor, does it show all the controls that are added? If yes, it is probably a type editor problem. In this case you should make sure that your list converts correctly to string. If not, you probably have to take a look at your serialilzer's deserialize method. – Sefe Dec 02 '16 at 18:48
  • @Sefe If the editor that you saying is the same of myForm so YES, all controls are added. If not, that editor is typeof(CustomTypeEditor) then NOT(the controls don't added). Can you show me how I can serialize/deserialize this data or to write a basic example like a list with two items ? What class I should put it? I'll post my TypeEditor in Update section. – Henrique Dec 02 '16 at 19:29

2 Answers2

1

WinForms design time support is a tricky subject. For better understanding: What happens is that the form that is being designed is turned into code. This process is called design time serialization. The serialization is performed by a CodeDomSerializer, which can be applied to the property with a DesignerSerializerAttribute. You can see your generated code in the auto-generated file for your form. When you do WinForms design time debelopment, you will sometimes have to check the serializer output (in the FormName.Designer.cs file). I'm sure in your case there will be no output that will add the controls to your container control (otherwise closing and reopening the form would not be a problem).

The default serializer can handle most of the serialization cases, mostly with the help of a TypeConverter. So you usually won't have to create your own serializer, but sometimes you have to do it. Your case is such a case.

The error message you have added to your post is a CodeDom error message, so it surely must come from the serializer, since this is wehere the source code is created. I think your issue in this particular case is that you don't add new items to your list, but other controls that already exist on your form. Normally, the default serializer will create a new item for each element in the collection. However, this is not what you need to do here, because you want to add existing items to the collection (that's why it works with a string, since it can be always created a literal).

Your solution is to create your own CodeDomSerializer that looks for the added controls through the design-time architecture (you will probably need an IReferenceService for that) and adds the CodeDom graph to add the existing item to your collection.

For example, where the default serializer creates code looking like this:

this.myControl.ActionButtons.Add(new Button());

Your code would have to look like this:

this.myControl.ActionButtons.Add(this.myActionButton);

That means first getting the name of your button (you only have the object in the collection) with the IReferenceService and then create a CodeDom graph that adds this button to your property. For that you will have to overwrite CodeDomSerializer.SerializeProperty and intercept the serialization of your ActionButtons property (be sure to call the base class for all other properties), where you can do your serialization.

Sefe
  • 13,731
  • 5
  • 42
  • 55
0

After some days I found the solution for this problem. I solved it creating my own Button collection that inherits of CollectionBase:

public class ButtonCollection : CollectionBase {

public CustomButton this[int i] {
  get { return InnerList[i] as CustomButton; }
  set { InnerList[i] = value; }
}

public ButtonCollection() {

}

public CustomButton Add(CustomButton bt) {
  InnerList.Add(bt);
  return bt;
}

public void AddRange(CustomButton[] bts) {
  InnerList.AddRange(bts);
}

public void Remove(CustomButton bt) {
  InnerList.Remove(bt);
}

public bool Contains(CustomButton bt) {
  return InnerList.Contains(bt);
}

public CustomButton[] GetValues() {
  CustomButton[] bts = new CustomButton[InnerList.Count];
  InnerList.CopyTo(bts);
  return bts;
}

I also made changes to the TypeConverter:

public override object ConvertTo(ITypeDescriptorContext context, CultureInfo info, object value, Type destType) {

  if ((destType == typeof(string)) && (value is CustomButton)) {
    CustomButton bt = (CustomButton) value;
    return bt.Name;
  }

  // this helped me a lot
  // here the object needs to know how to create itself
  // Type[0] can be overridden by Type[] { (your constructor parameterTypes) }
  // null can be overridden by objects that will be passed how parameter
  // third parameter is a value indicating if the initialization of the object is or not complete
  else if (destType == typeof(InstanceDescriptor)) {
    return new InstanceDescriptor(
      typeof(CustomButton).GetConstructor(new Type[0]),
      null,
      false
    );
  }

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

TypeConverter Decorator passed to CustomButton class:

[TypeConverter(typeof(CustomButtonConverter))]
public class CustomButton { 
...

I finalized all this following a fully example of custom collectionsEditor that can be found here.

Henrique
  • 198
  • 5
  • 11