Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DesignSurfaceExtended the third (chapter), how to implement a FileSystem Persistence Manager

0.00/5 (No votes)
3 Mar 2017 1  
Save and restore your design work using a persistence manager based on XAML code

Image 1

Abstract

The Article shows how to implement a persistence manager of a designed form.

Background

It is necessary to be familiar with C# programming. It's useful to know concepts used by the .NET Design services and to read my previously codeproject articles: DesignSurfaceExt and DesignSurfaceManagerExt

Introduction

First of all a very short introduction about what the hell IS this damned "DesignTime" ... I know, it's a bit strange that I write about this now ... at the third chapter of the series! :O But I've received more than one emails asking me to explain what exactly it is ... and, ok here we are.

Look at the picture:

Image 2

During RunTime your "compiled source code" is alive; let’s say that when you, or anyone in your stead, do execute the program from the CLI (Command Line Interface) or from the GUI (Graphical User Interface), that event really brings to life the program, because, from now on, the program is able to read / to write / to handle events and whatever it is instructed to do.

During CompileTime it is transformed from source code to binary code therefore, in some sense, we could say that it doesn’t exist.

During LatencyTime it’s really dead because it does nothing but wait to be executed.

Good… and during DesignTime? Well, the code is not alive yet because it is "under construction”, nor it is really dead, because it is able to respond to some sort of events: the "design events”, therefore we could say that it is half way alive (or half way dead, if you prefer).

Persistence into FileSystem with XAML

With the introduction of the WPF inside the .NET 3.0, the framework exposes the possibility to persistence the designed controls using an XML dialects: XAML. Two classes are very useful to manage XAML: XAMLReader and XAMLWriter.

Now some of you are about to complain: "Hey, Paolo. we have some good news for you: out there there is .NET 4.6! Wow! So, what are you talking about? .NET 3 is old , too old!"

I know, I know ... the beginning of this article has been written so many years ago, the original code was written using VisualStudio 2010! :( ...and only today I found the time to finish it (nowadays there is Visualstudio2017 RC, but for the article I used the 2013 version)! Do you believe? Years passed! Gosh! :\ So Don't panic: the article describes a persistence Manager based on .NET 4.5 Framework.

The controls will be serialized and deserialized using XAML file.

The story

Well, let's say you passed the whole afternoon to design a beautiful form with lots of coloured controls on its surface; now it's dinner-time and you decide to stop to work on your form. You want to continue to work tomorrow and... wait a minute! Suddenly you realize that you missing something that can save your work already done and restore it in a future time! What you basically need is a mechanism that can save your work into a persistence storage (like the File System, a DataBase, a Remote Repository accessible via web and so on) and that can restore your work from the persistence storage itself.

Image 3

The Persistence Manager under the hood

The code is developed using my previous pico Form designer as codebase, adding it the capability to persist the design work made by the (designer)user. Inside the posted VisualStudio solution you will find also the code about the DesignSurfaceExt, DesignSurfaceManagerExt and p(ico)Designer UserControl with the accompanying demo code.

As usual my recommendation is: feel free to use the code and to modify it as you like for building your own form designer (and let me know if you appreciate my work ;) ).

For those of you that are in hurry, the core are this two snippets of code:

public void Serialize( string storageFolderFullPath ) {
    foreach( IDesignSurfaceExt surf in this.DesignSurfaceManager.DesignSurfaces ) {
        IDesignerHost idh = (IDesignerHost) surf.GetIDesignerHost();

        string xmlfileName = idh.RootComponent.Site.Name;
        xmlfileName = Path.ChangeExtension( xmlfileName, this.ExtensionOfSerializedFile );
        string sFullPathFileName = Path.Combine( storageFolderFullPath, xmlfileName );
        if( idh != null && idh.RootComponent != null ) {
            IComponent root = idh.RootComponent;

            //- check if some  properties is about to be serialized as {x:Null}
            List<string> propertiesSetToNull = new List<string>();
            Control[] ctrlArray = GetChildControls( root as Form );
            foreach( Control c in ctrlArray ) {
                propertiesSetToNull.Clear();
                if( false == IsProperlySet( c, ref propertiesSetToNull ) )
                    SetToBlank( c, propertiesSetToNull );
            }

            try {
                using( TextWriter writer = File.CreateText( sFullPathFileName ) ) {
                    XamlServices.Save( writer, root );
                }
            }
            catch( Exception ex ) {
                throw;
            }
        }//end_if
    }//end_foreach
}</string>

You see, I use this one:

using( TextWriter writer = File.CreateText( sFullPathFileName ) ) {
    XamlServices.Save( writer, root );
}

to create the xaml file!

To deserialize from the XAML file:

public void Deserialize( string storageFolderFullPath ) {
    string lastFileUsed = string.Empty;
    try {
        DirectoryInfo di = new DirectoryInfo( storageFolderFullPath );
        IEnumerable<FileInfo> linqQuery = from f in di.GetFiles()
                                          where f.Name.EndsWith( this.ExtensionOfSerializedFile )
                                          select f;

        foreach( FileInfo fi in linqQuery ) {
            lastFileUsed = fi.FullName;

            try {
                using( TextReader reader = File.OpenText( lastFileUsed ) ) {
                    Object obj = XamlServices.Load( reader );
                    if( !(obj is Form) )
                        return;

                    Form fx = obj as Form;
                    foreach( Control c in fx.Controls ) {
                        c.Enabled = true;
                        c.Visible = true;
                    }

                    //- re-create the DesignSurfaces with controls inside
                    //-
                    //- Location and Size must be accessed via Control.Location and Control.Size
                    //- because the related  Control are dummy properties not sync to Form properties
                    //- Size is used during DesignSurface creation
                    //- while Location is used to modify the rootcomponent inside the DesignSurface
                    DesignSurfaceExt2 surface = (this as IpDesigner).AddDesignSurface<Form>(fx.Size.Width, fx.Size.Height, AlignmentModeEnum.SnapLines, new Size( 1, 1 ));
                    Form rootComponent = surface.GetIDesignerHost().RootComponent as Form;
                    (rootComponent as Control).Name = fx.Name;
                    //-
                    //-
                    //- this code doesn't reflect the location of the rootcomponent
                    //rootComponent.Location = (fx as IPCCommonProperties).P1ControlLocation;
                    //- instead we have to modify the Location via Reflection
                    Point newLoccation = (fx as Control).Location;
                    PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties( rootComponent );
                    //- Sets a PropertyDescriptor to the specific property
                    PropertyDescriptor pdS = pdc.Find( "Location", false );
                    if( null != pdS )
                        pdS.SetValue( rootComponent, newLoccation );
                    //-
                    //-
                    (rootComponent as Control).Enabled = true;
                    (rootComponent as Control).Visible = true;
                    (rootComponent as Control).BackColor = fx.BackColor;
                    (rootComponent as Control).Text = fx.Text;
                    //-
                    //- 
                    //- now deploy the control on the DesignSurface 
                    DeployControlsOnDesignSurface( surface, fx );
                }//end_using

            }//end_try
            catch( Exception ex ) {
                Debug.WriteLine( ex.Message );
                throw;
            }
        }//end_foreach
    }
    catch( Exception ex ) {
        throw new Exception( _Name_ + "::Deserialize() - Exception on deserializing file: " + lastFileUsed + "(see Inner Exception)", ex );
    }
}

Here I call:

using( TextReader reader = File.OpenText( lastFileUsed ) ) {
    Object obj = XamlServices.Load( reader );
    if( !(obj is Form) )
         return;
    Form fx = obj as Form;
...
}

to read from the XAML file and get my form. Yep! XAML has done it's dirty work and exits from the scene.

The remaining work is done by the method DeployControlsOnDesignSurface()

private void DeployControlsOnDesignSurface( DesignSurfaceExt2 surface, Form fx ) {
    //- steps:
    //- 1. deploy every control on DesignSurface
    //- 2. reparse the control setting the Parent Property

    //- step.1
    Control[] ctrlArray = GetChildControls( (fx as Control) );
    foreach( Control cc in ctrlArray ) {
        Control ctrlCreated = surface.CreateControl( cc.GetType(), cc.Size, cc.Location );
        Debug.Assert( !string.IsNullOrEmpty( cc.Name ) );
        //- pay attention to this fact:
        //- ctrlCreated.Site.Name is set by INameCreationService
        //- and may be different from the name of control we are cloning
        //- so set it accordingly, 
        //- because it will be used later to match the two controls
        ctrlCreated.Name = cc.Name;
        ctrlCreated.Site.Name = cc.Name;
        //- fill in the property of the 'designed' control using the properties of the 'live' control via Reflection
        string[] allowedProperties = { "Text", "BackColor" };
        Fill( ctrlCreated, cc, allowedProperties );
    }

    //-
    //-
    //- step.2
    //- iterate the components in a linear fashion (not hierarchically)
    foreach( IComponent comp in surface.GetIDesignerHost().Container.Components ) {
        if( comp is Form || comp.GetType().IsSubclassOf( typeof( Form ) ) )
            continue;

        string componentName = comp.Site.Name;
        foreach( Control c in ctrlArray )
            if( c.Name == componentName ) {
                //- if the parent is the rootcomponent (i.e. the Form)
                //- then do nothing, because the control is already deployed inside the Form
                if( c.Parent is Form || c.Parent.GetType().IsSubclassOf( typeof( Form ) ) )
                    break;

                //- the Parent is another control (different from the form)
                Control ctrParent = GetControlInsideDesignSurface( surface, c.Parent.Name );
                (comp as Control).Parent = ctrParent;
                break;
            }
    }//end_foreach
}

It merely cycle through every control hosted by the Form and, in turn, call the Fill() method which copy by Reflection some useful Properties: from the control "at RunTime" to the control at "DesignTime".

Using the code

Please refer to project "DemoConsoleForpDesigner" to see a "Form Designer", based on pDesigner, with a persistence layer in action. The code inside is heavily commented.

That's all folks!
Bye! (^_^)

History

04 March 2017 - First submission of the article/code.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here