5

I am creating a designer surface and loading the controls to a runtime. I am having issues when deserializing/loading the controls to the runtime.

All methods I have tried seem to have some type of issue.

Issued faced for example:

  • Controls are still bound of the design-time
  • Not all properties deserialize with all the properties, namely nested properties.
  • Control associations does seem to be followed, i.e. Button in a Panel, will not be in the panel anymore, even though the property is still the parent after loading.

I have created a sample Project on git here: Surface Designer Test

There are the main code snippets:

Serialization from the design-time

private void LoadRuntime(int type)
{
    var controls = surface.ComponentContainer.Components;
    SerializationStore data = (SerializationStore)surface.
        _designerSerializationService.Serialize(controls);
    MemoryStream ms = new MemoryStream();
    data.Save(ms);
    SaveData.Data = ms.ToArray();
    SaveData.LoadType = type;
    new RuntimeForm().Show();
}

public object Serialize(System.Collections.ICollection objects)
{
    ComponentSerializationService componentSerializationService = 
        _serviceProvider.GetService(typeof(ComponentSerializationService)) as 
        ComponentSerializationService;
    SerializationStore returnObject = null;
    using (SerializationStore serializationStore = 
        componentSerializationService.CreateStore())
    {
        foreach (object obj in objects)
        {
            if (obj is Control control)
            {
                componentSerializationService.SerializeAbsolute(serializationStore, obj);
            }
            returnObject = serializationStore;
        }
    }
    return returnObject;
}

Deserialization in runtime

Here is attempt with reflection:

MemoryStream ms = new MemoryStream(SaveData.Data);
Designer d = new Designer();
var controls = d._designerSerializationService.Deserialize(ms);

ms.Close();
if (SaveData.LoadType == 1)
{
    foreach (Control cont in controls)
    {
        var ts = Assembly.Load(cont.GetType().Assembly.FullName);
        var o = ts.GetType(cont.GetType().FullName);
        Control controlform = (Control)Activator.CreateInstance(o);
        PropertyInfo[] controlProperties = cont.GetType().GetProperties();
        foreach (PropertyInfo propInfo in controlProperties)
        {
            if (propInfo.CanWrite)
            {
                if (propInfo.Name != "Site" && propInfo.Name != WindowTarget")
                {
                    try
                    {
                        var obj = propInfo.GetValue(cont, null);
                        propInfo.SetValue(controlform, obj, null);
                    }
                    catch { }
                }
                else { }
            }
        }
        Controls.Add(controlform);
    }
}

Here is attempt with loading controls directly (still bound to the design-time):

MemoryStream ms = new MemoryStream(SaveData.Data);
Designer d = new Designer();
var controls = d._designerSerializationService.Deserialize(ms);
foreach (Control cont in controls)
    Controls.Add(cont);

I feel like I am missing a concept from the System.ComponentModel.Design framework.

I also do not believe there is a need to write a custom serializer for each control, as surely the already have this has Visual Studio is able to serialize all their properties as they are changed in the PropertyGrid and load them back when you run the program.

I'd love to serialize the designer into a .cs file, but how? How do you serialize controls/form and changed properties to a file like the VS designer, I tried and looked only to find xml and binary serializer. My ideal solution would be build a designer.cs with the CodeDom.

What is the correct way do accomplish this serialization between design-time and run-time?

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
TommyBoii
  • 77
  • 1
  • 11
  • You should serialize the whole form. Then load the form, compile and run it. – Reza Aghaei Dec 30 '19 at 16:19
  • @RezaAghaei It produces the exact sample issues :( – TommyBoii Dec 30 '19 at 16:48
  • By serializing, I mean serializing to a text file, exactly like VS does for you, and when you open the file, you should see something like `designer.cs` files. Then you need to load and compile and run it. – Reza Aghaei Dec 30 '19 at 16:54
  • @RezaAghaei I would love to do it that way, but how? How do you serialize controls/form and changed properties to a file like the designer, I tried and looked only to find xml and binary serializer. My ideal solution would be build a designer.cs with the CodeDom like you suggest. Could you provide a solution? – TommyBoii Dec 30 '19 at 17:03
  • Have you tried [`CodeDomComponentSerializationService`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.design.serialization.codedomcomponentserializationservice?WT.mc_id=DT-MVP-5003235&view=netframework-4.8)? – Reza Aghaei Dec 30 '19 at 17:29
  • Yes, I have, I got stuck pretty much like on this [post](https://stackoverflow.com/questions/47290751/serialization-with-codedomserializer-how-to-initialize-manager-object?rq=1) – TommyBoii Dec 30 '19 at 17:37

1 Answers1

10

Assuming you have a DesignSurface to show a Form as root component of the designer and having some components created at run-time by using CreateComponent method of IDesignerHost, here is how I approach the problem:

You can also extend the example a bit and use ISelectionService to get notified about selected components and change properties at run-time using a PropertyGrid:

enter image description here

Example - Generate C# code from DesignSurface at runtime

Here in this example, I'll show how you can host a windows forms designer at run-time and design a form containing some controls and components and generate C# code at run-time and run the generated code.

Please note: It's not a production code and it's just an example as a proof of concept.

Create the DesignSurface and host the designer

You can create the design surface like this:

DesignSurface designSurface;
private void Form1_Load(object sender, EventArgs e)
{
    designSurface = new DesignSurface(typeof(Form));
    var host = (IDesignerHost)designSurface.GetService(typeof(IDesignerHost));
    var root = (Form)host.RootComponent;
    TypeDescriptor.GetProperties(root)["Name"].SetValue(root, "Form1");
    root.Text = "Form1";

    var button1 = (Button)host.CreateComponent(typeof(Button), "button1");
    button1.Text = "button1";
    button1.Location = new Point(8, 8);
    root.Controls.Add(button1);

    var timer1 = (Timer)host.CreateComponent(typeof(Timer), "timer1");
    timer1.Interval = 2000;
    var view = (Control)designSurface.View;
    view.Dock = DockStyle.Fill;
    view.BackColor = Color.White;
    this.Controls.Add(view);
}

Generate C# code using TypeCodeDomSerializer and CSharpCodeProvider

This is how I generate code from design surface:

string GenerateCSFromDesigner(DesignSurface designSurface)
{
    CodeTypeDeclaration type;
    var host = (IDesignerHost)designSurface.GetService(typeof(IDesignerHost));
    var root = host.RootComponent;
    var manager = new DesignerSerializationManager(host);
    using (manager.CreateSession())
    {
        var serializer = (TypeCodeDomSerializer)manager.GetSerializer(root.GetType(),
            typeof(TypeCodeDomSerializer));
        type = serializer.Serialize(manager, root, host.Container.Components);
        type.IsPartial = true;
        type.Members.OfType<CodeConstructor>()
            .FirstOrDefault().Attributes = MemberAttributes.Public;
    }
    var builder = new StringBuilder();
    CodeGeneratorOptions option = new CodeGeneratorOptions();
    option.BracingStyle = "C";
    option.BlankLinesBetweenMembers = false;
    using (var writer = new StringWriter(builder, CultureInfo.InvariantCulture))
    {
        using (var codeDomProvider = new CSharpCodeProvider())
        {
            codeDomProvider.GenerateCodeFromType(type, writer, option);
        }
        return builder.ToString();
    }
}

For example:

var code = GenerateCSFromDesigner(designSurface);

Run the code sing CSharpCodeProvider

Then to run it:

void Run(string code, string formName)
{
    var csc = new CSharpCodeProvider();
    var parameters = new CompilerParameters(new[] {
    "mscorlib.dll",
    "System.Windows.Forms.dll",
    "System.dll",
    "System.Drawing.dll",
    "System.Core.dll",
    "Microsoft.CSharp.dll"});
    parameters.GenerateExecutable = true;
    code = $@"
        {code}
        public class Program
        {{
            [System.STAThread]
            static void Main()
            {{
                System.Windows.Forms.Application.EnableVisualStyles();
                System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
                System.Windows.Forms.Application.Run(new {formName}());
            }}
        }}";
    var results = csc.CompileAssemblyFromSource(parameters, code);
    if (!results.Errors.HasErrors)
    {
        System.Diagnostics.Process.Start(results.CompiledAssembly.CodeBase);
    }
    else
    {
        var errors = string.Join(Environment.NewLine,
            results.Errors.Cast<CompilerError>().Select(x => x.ErrorText));
        MessageBox.Show(errors);
    }
}

For example:

Run(GenerateCSFromDesigner(designSurface), "Form1");
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • Thank you for a very complete and helpful answer! This seems to solve all my problems at run time, although recreates another issue, reloading the generated code for the designer. I have tried all morning to get the generated code from the GenerateCodeFromType back to the TypeCodeDomSerializer Deserializer method, closed I get is the CodeUnit Class. For completeness would be so kind to expand your answer to include this? – TommyBoii Dec 31 '19 at 12:13
  • Let's say you have generated a c# code using this designer, then do you want to be abke to load the designer using that generated code? – Reza Aghaei Dec 31 '19 at 12:21
  • Exactly, like VS Designer would do. As I would not expect to to compile the generated code like it does in the runtime. I would presume it uses the TypeCodeDomSerializer Deserialize Method but to it from generate code but to CodeTypeDeclaration, is where I am getting stuck. Unless there is another way? – TommyBoii Dec 31 '19 at 12:27
  • @TommyBoii I gave it a try (and failed). It looks like it makes this question too broad and unfocused. I suggest you post another question, including your attempt to load the designer using a piece of serialized code. If you think it may help. refer to this question as well. – Reza Aghaei Dec 31 '19 at 16:43
  • If you asked a new question, feel free to notify me, I'll also give it a try, while you try your chance to get answer form other community members as well. – Reza Aghaei Dec 31 '19 at 16:44
  • I found the answer, you need to use a binary or xml serializer on the CodeTypeDeclaration output and keep that, and only generate the CodeDom when you want to use the runtime. Thank you for all your help. – TommyBoii Jan 01 '20 at 17:22
  • I know how to do it by XML serialization (save designer to XML and load it from xml). But what I was trying to do was using C# code dome serializer (save designer to .cs and load it from .cs) and my problem is in deserializing from C# code to designer. – Reza Aghaei Jan 01 '20 at 17:47
  • So my question is: did you choose to use XML serialization? – Reza Aghaei Jan 01 '20 at 17:49
  • I guess you serialize to xml to be able to load into designer again, then to run, you generate C# code. which works, but it's different from what I was trying to do. I was trying to do it exactly like VS, by saving and loading .cs code, but it's not trivial. – Reza Aghaei Jan 01 '20 at 17:57
  • Hi Reza, I too have an XML serializer/deserializer working for a Basic loader and a C# serializer working from a CodeDom loader that I create on-the-fly from a Basic loader's surface. I would love to know how Microsoft deserializes (apparently directly?) from C# to a designer without passing through XML. Makes you wonder why Microsoft wouldn't make that deserializer code (from C#,VB.NET, etc.) available to anyone trying to leverage the component designer classes for their own projects... It's a bit disappointing to have to serialize to XML for persistence and C# for compilation... – Jazimov Jan 27 '20 at 23:27
  • Hi @Jazimov, as far as I see, some of the designer feature implementations are part of Visual Studio and they are not part of the framework. For example you can take a look at `Microsoft.VisualStudio.Design.dll` and see classes like `VSCodeDomProvider` and `VSCodeDomParser` or `FileCodeModel` in `EnvDTE.dll`. `EnvDTE` dependencies are OK because it's redistribute-able and they are public interfaces/classes and but for `Microsoft.VisualStudio.Design` the classes are internal and I'm not sure if the assembly is redistribute-able. But you probably can copy the code from decompiled assembly. – Reza Aghaei Jan 28 '20 at 07:42
  • @RezaAghaei Sorry, I actually thought I replied to this. yes I decided to use xml in the end. Thank you for all your help. I've been playing around with trying to do it like VS as well but as you said, its really not trival. – TommyBoii Jul 21 '21 at 11:25
  • @RezaAghaei, I had some more time to play around with this scenario again. if you or anyone else is interested. I created another post with a related issue trying to generated an resx file that i am trying to solve. https://stackoverflow.com/questions/68504578/winform-surface-designer-how-to-generater-resx-file-from-component-serializer – TommyBoii Jul 23 '21 at 20:15
  • @TommyBoii, sure, if I have any idea, I'll share. – Reza Aghaei Jul 24 '21 at 15:50
  • that's impressive work! I have to make a small but the project is not using C# but node.js/electron. Regard to the internals, how does the library load that form in the GUI designer where the user can drag-and-drop the controls? is this a bunch of controls that simulate the executable or is this the executable process itself loaded in the application put in drag-and-drop target area? – Jack May 01 '22 at 04:52
  • 1
    @Jack thanks for the feedback. All the Windows Forms designer features are implemented in .NET class libraries (in .NET framework and VS libs) and I guess you cannot use them. The way that windows Forms Designer works is: It create real instances of the components at design time, but put them behind an overlay, so that the user cannot really interact with them; and all the selection and changing their properties will be done indirectly. The designer serializes the design into a code, and later it's able to deserialize the code and create the components at design-time. – Reza Aghaei May 02 '22 at 00:16
  • @RezaAghaei I see, thanks for giving more details again how it works. Now regard to the GUI builder, do you know how it works, for example, to make that windows form where we drag the controls to, it host the actual compiled exe (compiling only the .Designer.cs files) or something else? how is that windows form in the GUI designer implemented? if it does uses the load approach, do you have any idea how is that done? by now I know I can do that using something like this https://pastebin.com/7xKQjXxr but I'm not sure they use this approach. Maybe it's hacky, i don't know – Jack May 02 '22 at 03:21
  • @Jack When you open Form1 in designer, the designer deserializes the Form1.Designer.cs, and then it knows everything about the form. It creates an instance of **base** class of the form and add it to the designer (simply like adding any other control at run-time) and then add the controls and set their properties. So basically it doesn't load the whole exe, it just create an instance of each form in designer. For example you may have a lot of syntax errors in the code, and the code may even not build, but the designer may be able to load it. – Reza Aghaei May 02 '22 at 08:52
  • I believe my other answer [here](https://stackoverflow.com/a/32299687/3110834) will help you to learn more on how designer works. You can see a very interesting example (in my point of view ) which shows how designer can load a form from a .cs file while the file could not be compiles and it's full of error. – Reza Aghaei May 02 '22 at 08:54
  • @RezaAghaei thanks for the enlightenment! very insightful, you knowledge about the internals is amazing, love it. I also need to make a small GUI builder so I was getting to know how VS or even the old but gold delphi 7 did that. – Jack May 02 '22 at 17:40