Hosting Windows Forms Designer - Serialize designer at runtime and generate C# code
Asked Answered
S

1

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?

Superorder answered 30/12, 2019 at 11:9 Comment(6)
You should serialize the whole form. Then load the form, compile and run it.Leveille
@RezaAghaei It produces the exact sample issues :(Superorder
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.Leveille
@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?Superorder
Have you tried CodeDomComponentSerializationService?Leveille
Yes, I have, I got stuck pretty much like on this postSuperorder
L
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");
Leveille answered 30/12, 2019 at 21:53 Comment(20)
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?Superorder
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?Leveille
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?Superorder
@Superorder 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.Leveille
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.Leveille
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.Superorder
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.Leveille
So my question is: did you choose to use XML serialization?Leveille
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.Leveille
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...Affiche
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.Leveille
@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.Superorder
@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. #68505078Superorder
@TommyBoii, sure, if I have any idea, I'll share.Leveille
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?Bisutun
@Bisutun 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.Leveille
@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 pastebin.com/7xKQjXxr but I'm not sure they use this approach. Maybe it's hacky, i don't knowBisutun
@Bisutun 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.Leveille
I believe my other answer here 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.Leveille
@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.Bisutun

© 2022 - 2024 — McMap. All rights reserved.