Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Fluid Geometry - An Animation Library and Configuration Application

5.00/5 (48 votes)
9 May 2012CPOL38 min read 1   2.5K  
A task-oriented review of an animation library and the application which uses it

Image 1 Image 2

Image 3 Image 4

Introduction

This article is about an animation application created over the course of a couple months. The application is called Fluid Geometry, and it is being given to the world via www.fluidgeometry.com. It was written in C# using Visual Studio .NET 2003.

Fluid Geometry provides an intuitive way for users to create animated “scenes” that can be saved and then viewed at a later time. A scene consists of one or more “paths” that move around in the scene. A path contains one or more “movers” that navigate the path. There are several types of paths that you can use to construct a scene; such as a sine wave, ellipse, infinity symbol, and others. Each path has a variety of settings that you can configure to customize its behavior. You can combine paths together in ways that create some truly stunning animated patterns. For more information regarding what Fluid Geometry is all about, feel free to visit www.fluidgeometry.com.

Fluid Geometry is broken into three physical pieces:

  • FluidGeometryLib.dll – The heart of the application is an animation library which provides rendering services. If you were so inclined, you could make use of that assembly in your own application.
  • Fluid Geometry.exe – The user runs this executable to launch the configuration form in which he/she can create, modify, save, erase, and view custom scenes.
  • Fluid Geometry Screensaver.scr – A screensaver which displays scenes.

How this Article is Structured

Considering the size and modest complexity of the application, it would have been counterproductive to write about every aspect of it (not to mention tedious). With that in mind, I decided that the best way to structure this article would be as a collection of programming tasks. I chose what I considered to be the most interesting or challenging aspects of the application, and focused on them. The article contains multiple topics, each of which contains one or more sections. Before getting into the details of how certain things work, we will start off with a brief overview of the animation library’s architecture. That will provide the context needed to understand why things were implemented the way that they were.

I hope that this task-based format will prove beneficial to both the pragmatist seeking a quick answer, and the more thorough “cover-to-cover” reader alike. Below, you will find a collection of links to the different programming tasks explored in this article:

Before getting into the remainder of the article, I suggest that you download and play with Fluid Geometry first. You can download the files from the links at the top of this article or from www.fluidgeometry.com. The rest of this article will be much more meaningful if you have seen the application in action. Plus, it’s fun! When using the Fluid Geometry configuration application for the first time, be sure to read the helpful descriptions in the StatusBar as you move the mouse cursor over different controls on the Form.

Architecture

The classes that constitute the animation library are straightforward. Below, you will find a rudimentary class diagram illustrating the key classes and their relationships:

Image 5

As you can see above, there are very few core types involved with the animation logic. Here is a brief rundown of the different types referenced in the diagram above.

Scene

The Scene class is the key player; it represents the entire animated scene that the user creates with the configuration application. A Scene holds a reference to an ISceneHost, MovementManager, and a PathCollection. It stores the default settings for paths; such as the default mover image, the default mover speed, etc. Scene also exposes methods for saving and loading scenes to/from disk.

ISceneHost

ISceneHost is an interface used to decouple the Scene class from any particular rendering surface. A Scene retrieves a Graphics object to render with from this interface, along with other host-specific information. We will delve deeper into why this interface is important in the Decoupling a Scene from its Host (ISceneHost) section later on.

MovementManager

MovementManager is responsible for instructing the paths in the scene to move at a regular interval. Since different paths in the same scene can move at different speeds, this class has the interesting task of ensuring that every path moves only when it should and does not erase sections of slower moving paths.

PathCollection

This class is a type-safe wrapper of an ArrayList with a few extra members. Contrary to the image above, a PathCollection can contain any number of paths, not just two.

Path

The Path class represents a path in a scene. Path is an abstract class which provides common functionality required by all path types. Path and Path-derived classes are the only places in the animation library containing the mathematical logic which produces the shapes that Movers follow. You can develop your own Path-derived classes and they will automatically be incorporated into the configuration application, provided that you apply a few attributes. There are currently five Path-derived classes that you can include in scenes. Path is primarily responsible for coordinating the motion of the Movers in its MoverCollection.

MoverCollection

This class is a type-safe wrapper of an ArrayList with a few extra members. Contrary to the image above, a MoverCollection can contain up to five hundred Movers, not just three.

Mover

Mover represents a visual object which exists on a path. Mover is the only class in the animation library with a visual representation, with the exception of the background color of the scene. Movers, as the name suggests, “move” along the path they belong to (the “movement” is really just a result of manipulating coordinates with trigonometric equations). A Mover is displayed with an image, which can be specified on the scene or, optionally, on a per-path basis. Every mover on a given path has the same image and size as every other mover on that path.

I am not going to discuss the design of the configuration application because it is basically just a monolithic Form-derived class with a few helper classes. However, there is some very interesting work being done in the configuration application. The next topic discusses exactly that.

Using Reflection to Create an Extensible User Interface

After reading this topic you will know:

  • How an application can benefit from having its user interface loosely coupled to the underlying object model
  • How to query assemblies at runtime for information about the types they contain
  • How to discover properties of a type which are decorated with a custom attribute
  • How to get and set the value of dynamically discovered properties
  • How to create and load controls at runtime based on a set of properties found via reflection
  • How to validate user input based on information known only at runtime

One of the most challenging aspects of the entire undertaking was creating a user interface which both makes it simple for users to create scenes, and does not need to be updated when a new type of path is created. A major goal was to have new Path-derived classes be automatically “plugged into” the existing infrastructure. As you can see in the screenshot below, when a path is added to a scene, it can be configured via controls in the “Path Attributes” GroupBox.

Image 6

When the user selects one of the paths added to the scene, the “Path Attributes” GroupBox is populated with controls representing the various settings exposed by the selected path. Keep in mind that different types of paths expose different custom settings, so the number and types of controls to be loaded depends on the type of the selected path. The dynamically loaded controls display and update the value of their corresponding properties. Dynamically loaded TextBox controls, which represent properties with numeric data types, are validated based on constraints determined at runtime.

The net result of this functionality is that paths do not need to be associated with a set of controls at design-time. In fact, paths know nothing about how their settings are displayed in the configuration application and the configuration application knows very little about the paths it displays the settings of. When a new Path-derived class is created, there is no need to arrange a set of controls representing the customizable properties of that new type at design-time. This automated approach to presenting path settings also ensures that the user interface is consistent across all path types, thus making it easier for users to learn and navigate the application’s user interface.

The next several sections of the article discuss how this flexible user interface was implemented.

Discovering Path Types at Runtime

The Fluid Geometry configuration application does not know the different type of paths that exist until runtime. Determining the Path-derived classes that exist at runtime is done with reflection. When the configuration application is loading, it searches the FluidGeometryLib.dll assembly for any Path-derived classes and populates the “Path Types” ListBox with what it finds, as shown below:

C#
private void LoadPathTypesData()
{
    // If the "Path Types" ListBox is empty,
    // fill it with the names of any Path-derived
    // types found in the assembly containing
    // the Path type (a.k.a. FluidGeometryLib.dll).
    //
    if( this.lstPathTypes.Items.Count == 0 )
        foreach( Type type in Assembly.GetAssembly( typeof(Path) ).GetTypes() )
            if( typeof(Path).IsAssignableFrom( type ) && ! type.IsAbstract )
                this.lstPathTypes.Items.Add( new PathTypeWrapper( type ) );
}

The PathTypeWrapper class wraps a Type object and other pieces of information about a particular Path-derived class. The “Path Types” ListBox is populated with PathTypeWrapper objects so that when the user adds a path to a scene, all of the information needed for the path to be created is in one convenient place.

Querying Path Types for Custom Settings

Once the configuration Form has loaded and the “Path TypesListBox has been populated with all of the available types of paths, the user might add a path to the scene being created. For example, the user might double click on the “Ellipse” item in the “Path TypesListBox. In response to that double click, an EllipsePath object is created and added to the scene. At that point, the “Paths In SceneListBox is given an item which represents the newly created ellipse path, and that item will be selected. Once the item has been selected, it is necessary to display controls for all of the selected path’s settings.

Before the controls can be loaded, however, it is necessary to find out what custom settings the selected path has to offer. Not all path types have the same settings, so reflection is used again to discover the eligible properties. This is accomplished by applying the PathSettingAttribute attribute to properties on path classes which should be configurable by the user, and then, at runtime, querying the selected path type for all properties decorated with that attribute. The code snippet below demonstrates this technique.

C#
// This code is from the get method
// of the CustomSettings property in the PathTypeWrapper class.
ArrayList propsWithAttribute = new ArrayList();
foreach( Type type in this.TypeHierarchy )
{
    PropertyInfo[] propInfos = type.GetProperties( 
        BindingFlags.DeclaredOnly | 
        BindingFlags.Public | 
        BindingFlags.Instance );

    foreach( PropertyInfo pi in propInfos )
    {
        object[] objArr = 
          pi.GetCustomAttributes( typeof(PathSettingAttribute), 
          true );
        if( objArr.Length > 0 )
        {
            PathSettingAttribute pathSetting = 
                objArr[0] as PathSettingAttribute;
            propsWithAttribute.Add( new 
                PathSettingInfo( pi, pathSetting ) );
        }
    }
}
    
// Helper property in PathTypeWrapper class.
private Type[] TypeHierarchy
{
    get
    {
        ArrayList typeHierarchy = new ArrayList();
        Type t = this.Type;
        while( t != null )
        {
            typeHierarchy.Add( t );
            t = t.BaseType;
        }
        typeHierarchy.Reverse();
        return typeHierarchy.ToArray( typeof(Type) ) as Type[];
    }
}

// An example of a custom setting
// on the SineWavePath class using the PathSetting attribute.
// The arguments to the PathSetting constructor
// are the friendly name of the setting, the 
// minimum value, and the maximum value.
[PathSetting("Number of Wave Peaks", 0, 40)]
public int NumPeaks
{
    get 
    {
        //...
    }
    set 
    {
        //...
    }
}

As you can see in the code above, the CustomSettings property loops over every type in the type hierarchy of a Path-derived class and stores information about every property decorated with the PathSetting attribute. The individual types in the type hierarchy are inspected one at a time to ensure that every eligible property is found. They are inspected from the root type to the most derived so that the custom settings in the abstract Path class are found first. Finding those properties first ensures that the controls which represent them will always appear at the same relative position in the “Path AttributesGroupBox.

Creating Controls Dynamically Based on Path-Specific Settings

Once the custom settings of the selected path have been determined, it is possible to create controls that can be used to display and modify the values of those settings. Some of the controls in the “Path AttributesGroupBox exist at design-time; such as the TrackBar which indicates the mover diameter, the PictureBox which displays the mover image, and others. Every path has a few settings in common, so the controls for those settings are not added or removed at runtime (with a few exceptions). The dynamically loaded controls are parented to a Panel in the “Path AttributesGroupBox.

The type of control created for a custom setting is based on the data type of the setting (i.e., the type of the property). Boolean properties are given CheckBoxes, Enum properties are given ComboBoxes, and everything else is given a TextBox. Since it would be pointless to just display a control, Labels are added alongside ComboBoxes and TextBoxes so that the user knows which setting a control represents. The Labels display a setting’s “friendly name,” as specified in the PathSetting attribute applied to the setting property. Below is a screenshot of some dynamically loaded controls in the “Path SettingsGroupBox when a SineWave path is selected:

Image 7

Below is the logic which creates and loads the path-specific controls:

C#
private void LoadControlsForPreviewPath()
{
    PathTypeWrapper wrapper = 
      this.lstPathsInScene.SelectedItem as PathTypeWrapper;
    Path path = this.PreviewPath;

    if( wrapper == null || path == null )
        return;

    // First remove the existing controls that
    // were loaded for the previous Preview Path.
    this.RemoveDynamicallyLoadedControls();
    const int GAP = 6;
    Control ctrl = null;
    int tabIdx = this.pnlPermanentPathSettingsControlHost.TabIndex;
    Point pt = new Point(
        this.pnlPermanentPathSettingsControlHost.Location.X,
        this.pnlPermanentPathSettingsControlHost.Location.Y + 
        this.pnlPermanentPathSettingsControlHost.Height + GAP );
 
    foreach( PathSettingInfo settingInfo in wrapper.CustomSettings )
     { 
        if( settingInfo.PropertyInfo.PropertyType == typeof(bool) )
        {
            #region Create CheckBox

            CheckBox chkBox = new CheckBox();
            chkBox.FlatStyle = FlatStyle.System;
            chkBox.Checked = (bool)settingInfo.GetValue( path ); 
            chkBox.CheckedChanged += 
                   new EventHandler( OnCheckBoxCheckedChanged );
            chkBox.Text = settingInfo.PathSetting.FriendlyName;

            ctrl = chkBox;

            #endregion // Create CheckBox
        }
        else if( settingInfo.PropertyInfo.PropertyType.IsEnum )
        {
            #region Create ComboBox

            ComboBox combo = new ComboBox();
            combo.DropDownStyle = ComboBoxStyle.DropDownList;
            string[] names = 
              Enum.GetNames( settingInfo.PropertyInfo.PropertyType );
            string currentValue = 
              settingInfo.GetValue( path ).ToString();
            foreach( string name in names )
            {
                int idx = combo.Items.Add( name );
                if( name == currentValue )
                    combo.SelectedIndex = idx;
            }
            combo.SelectedIndexChanged += 
                  new EventHandler( OnComboBoxSelectedIndexChanged );

            ctrl = combo;

            #endregion // Create ComboBox
         }
        else
        {
            #region Create TextBox

            TextBox txt = new TextBox();
            txt.Text = settingInfo.GetValue( path ).ToString();
            txt.Enter += new EventHandler( this.OnTextBoxEnter );
            txt.KeyPress += 
                new KeyPressEventHandler( this.OnTextBoxKeyPress );
            txt.TextChanged += 
                new EventHandler( this.OnTextBoxTextChanged );
            txt.Leave += new EventHandler( this.OnTextBoxLeave ); 
 
            ctrl = txt;

            #endregion // Create TextBox
        }

        if( ctrl is CheckBox == false )
        {
            #region Add Label To Panel

            Label lbl = new Label();
            this.pnlPathAttributesControlHost.Controls.Add( lbl );
            lbl.Text = settingInfo.PathSetting.FriendlyName;
            lbl.FlatStyle = FlatStyle.System;
            lbl.AutoSize = true;
            lbl.Location = pt;
            pt.Offset( 0, lbl.Height + 1 );

            #endregion // Add Label To Panel
        }

        #region Add Control To Panel

        this.pnlPathAttributesControlHost.Controls.Add( ctrl );
        ctrl.Tag = settingInfo; 
        ctrl.Width = this.txtNumMoversOnPath.Width;
        ctrl.Location = pt;
        ctrl.TabIndex = tabIdx++;
        this.AttachMouseEnterAndLeaveHandlers( ctrl );
        pt.Offset( 0, ctrl.Height + GAP );

        #endregion // Add Control To Panel
    }
}

The code above loops over the array of PathSettingInfo objects returned by the CustomSettings property of the PathTypeWrapper class, which we examined in the previous section. Each custom setting is described by a PathSettingInfo object. Depending on the data type of the setting, a certain type of control is created and configured. If the control does not have a “built-in label” (as the CheckBox does) then the layout logic will position a Label above the control. The display name of the setting is retrieved from the PathSettingInfo object’s FriendlyName property. The location of each control is determined by the ‘ptPoint variable, which is offset after each control is added to the Panel. As a convenience to the user, the TabIndex of each control is set to one higher than the control created before it. This allows for intuitive keyboard navigation.

One last thing to take note of is that the Tag of each control is set to the PathSettingInfo object which represents the same setting represented by the control. This will be important later when the user edits the value of a dynamically loaded control. The PathSettingInfo object will provide the context needed for updating the appropriate property on the path and performing input validation. The next two sections discuss how the input values for custom settings are validated and how the settings are updated.

Validating Input Values Against Arbitrary Constraints

As mentioned previously, path settings with numeric data types are displayed in TextBox controls in the “Path AttributesGroupBox. Since the user can type any value into a TextBox, it is necessary to restrict input to a number which falls within a given range. If the property’s data type is integral (i.e., not floating point) then only whole numbers should be allowed.

These circumstances present some interesting problems:

  • Flexible Editing - The user must not be forced to enter/delete characters in a particular order. The validation system must be flexible enough to allow the user to type values in as he/she sees fit. Some people delete old numbers first and then type new numbers in, other people type new numbers in first and then delete old numbers.
  • Validation - The setting-specific numeric ranges must be dynamically discovered and enforced. A TextBox should never have an invalid value if it does not have the input focus.
  • Informing the User - The user needs a way to know when an input value is invalid and also must be able to easily find out what a setting’s minimum and maximum values are.

The code listed in the previous section demonstrates how the dynamically loaded TextBoxes are created and added to the control hierarchy. When a TextBox control is created, the ConfigurationForm attaches handlers to a few of its events, as shown below:

C#
TextBox txt = new TextBox();
txt.Enter += new EventHandler( this.OnTextBoxEnter );
txt.KeyPress += new KeyPressEventHandler( this.OnTextBoxKeyPress );
txt.TextChanged += new EventHandler( this.OnTextBoxTextChanged );
txt.Leave += new EventHandler( this.OnTextBoxLeave );

When a TextBox is entered, the ConfigurationForm’s OnTextBoxEnter method will be invoked. It simply caches the current value of the TextBox into a member variable. Since the rule is that a TextBox cannot have an invalid value if it does not have the input focus, when a TextBox receives the input focus it will have a valid value. That value might be used later on if the user enters an invalid value and then leaves the control. Here is the Enter event handler:

C#
private void OnTextBoxEnter(object sender, System.EventArgs e)
{
    this.lastValidValueInTextBox = (sender as TextBox).Text;
}

Below is the handler for the KeyPress event of a dynamically loaded TextBox:

C#
private void OnTextBoxKeyPress( object sender, KeyPressEventArgs e )
{
    // IsControl returns true for the Backspace key,
    // which is an allowable key.
    if( ! Char.IsDigit( e.KeyChar ) && e.KeyChar != '.' 
                     && ! Char.IsControl( e.KeyChar ))
        e.Handled = true;
}

The code above prevents a TextBox from being notified of any keystrokes that are not numeric characters, a period ‘.’, or a “control key” such as Backspace. This is the first level of input validation. Since these TextBoxes should only contain numeric values, it makes sense to prevent any non-numeric characters from entering them through the keyboard. This logic will not prevent the user from pasting non-numeric data into the TextBox, but other layers of validation handle that issue.

Once the text in a TextBox has been changed, the ConfigurationForm’s OnTextBoxTextChanged handler is invoked. If that handler determines that the TextBox represents a custom path setting, it calls the following method which performs the bulk of the data validation:

C#
private void UpdateSettingInPreviewPath( TextBox textBox )
{
    // This is necessary because otherwise it would be impossible
    // to clear out the textbox or start a decimal number with a
    // decimal point (as opposed to starting it with "0.").
    //
    string text = textBox.Text.Trim();
    if( text == "" || text == "." )
        return;

    PathSettingInfo settingInfo = textBox.Tag as PathSettingInfo;
    ValueType val = this.ConvertPathSettingInputValue( text, settingInfo );
    if( val != null )
    {
        this.isPathSettingValueValid = true;
        settingInfo.SetValue( this.PreviewPath, val );
        this.lastValidValueInTextBox = textBox.Text;
        this.errorProvider.SetError( textBox, String.Empty );
    }
    else
    {
        this.isPathSettingValueValid = false;
        if( settingInfo.PathSetting.HasMaxValue && 
            settingInfo.PathSetting.HasMinValue )
        {
            string errorMsg = String.Format( 
                "This value must be between {0} and {1}.", 
                settingInfo.PathSetting.MinValue.ToString(), 
                settingInfo.PathSetting.MaxValue.ToString() );

            this.errorProvider.SetError( textBox, errorMsg );
        }
    }
}

private ValueType ConvertPathSettingInputValue(string inputText, 
                                    PathSettingInfo settingInfo)
{
    bool isValid = true;
    ValueType val = null;
    try
    {
        Type propType = settingInfo.PropertyInfo.PropertyType;
        val = Convert.ChangeType( inputText, propType ) as ValueType;
        IComparable comparableVal = val as IComparable;

        if( settingInfo.PathSetting.HasMinValue )
        {
            ValueType min = Convert.ChangeType( 
            settingInfo.PathSetting.MinValue, propType ) as ValueType;
            isValid = comparableVal.CompareTo( min as IComparable ) >= 0;
        }
        if( isValid && settingInfo.PathSetting.HasMaxValue )
        {
            ValueType max = Convert.ChangeType( 
            settingInfo.PathSetting.MaxValue, propType ) as ValueType;
            isValid = comparableVal.CompareTo( max as IComparable ) <= 0;
        }
    }
    catch
    {
        isValid = false;
    }
    return isValid ? val : null;
}

The code above tests if the input value is within the acceptable range. If it is, the setting on the selected path (a.k.a. the “preview path”) is updated and the new valid value is cached. If the new value is invalid, the ErrorProvider component is told to display an error icon next to the TextBox. The tooltip of the error icon informs the user of the setting’s acceptable range. Using the ErrorProvider, as opposed to immediately rejecting invalid values, allows for flexible editing because the user is able to temporarily type invalid values into a TextBox. It also serves as a way of informing the user of the valid range of values for the setting that is being modified.

The ConvertPathSettingInputValue method is where the input value is tested for validity. If the input value cannot be converted from a string to the type of the property being set, then it is immediately considered invalid. If the path setting has a MinValue or MaxValue, the converted input value is compared against them. The minimum and maximum values for a path setting are specified in the PathSettingAttribute constructor, as seen below:

C#
[PathSetting("Number of Wave Peaks", 0, 40)]
public int NumPeaks
{
    get
    {
        //...
    }
    set
    {
        //...
    }
}

These values are exposed as public properties on the PathSettingAttribute class, which is how they are retrieved during validation.

Lastly, when the user moves the input focus to another control, the Leave event of the TextBox is handled. If the value contained in the TextBox is known to be invalid or is missing, then the cached valid value is restored. The following code demonstrates this:

C#
private void OnTextBoxLeave(object sender, System.EventArgs e)
{
    TextBox textBox = sender as TextBox;
    if( ! this.isPathSettingValueValid || textBox.Text.Length == 0 )
    {
        textBox.Text = this.lastValidValueInTextBox;
    }
}

Once an input value is successfully validated, the path setting is updated with the new value. The code which performs the update is called from the UpdateSettingInPreviewPath method, as seen above. The next section explores how the property is set, even though the property name is unspecified at compile-time.

Updating Path Settings with Reflection

Once the user has specified a new value for a path setting, it is necessary to set the corresponding property to that new value. Since the configuration application is not aware of the path types and custom path settings at compile-time, it is necessary to use reflection to set the property. All of the relevant reflection code is in the PathSettingInfo class. When a PathTypeWrapper creates a PathSettingInfo object, it passes a System.Reflection.PropertyInfo object to it, as seen in the Querying Path Types for Custom Settings section. Here is the relevant snippet:

C#
foreach( PropertyInfo pi in propInfos )
{
    object[] objArr = 
       pi.GetCustomAttributes( typeof(PathSettingAttribute), true );
    if( objArr.Length > 0)
    {
        PathSettingAttribute pathSetting = objArr[0] as PathSettingAttribute;
        propsWithAttribute.Add( new PathSettingInfo( pi, pathSetting ) );
    }
}

PathSettingInfo uses that PropertyInfo object to get and set the value of the property on a particular path object. The SetValue method is where the value on the path is actually updated, as seen below:

C#
public void SetValue( Path path, object value )
{
    this.PropertyInfo.SetValue( path, value, new object[0] );
}

The SetValue method simply delegates the work off to the PropertyInfo.SetValue method.

Implementing this indirect approach to path configuration was worth the extra effort. The configuration application does not need to know about the different path types that exist at compile-time. With the help of reflection and custom attributes, it is possible to decouple the configuration logic from the animation library. You can create and modify Path-derived classes without needing to be concerned about breaking the configuration application.

Serialization/Deserialization

After reading this topic you will know:

  • What serialization and deserialization are, and some problems that they can solve.
  • How to serialize/deserialize an object graph.
  • Why and how certain objects should be excluded from the serialization process.
  • Why and when custom serialization/deserialization should be used.
  • How to implement custom reflection-based serialization/deserialization using the ISerializable interface.
  • How to perform post-deserialization tasks with the DeserializationCallback interface.

The configuration application would be rather frustrating if it did not allow the user to save a scene and view it at a later time. The Fluid Geometry Screensaver would not be able to display user-defined scenes if there was no way to save a scene to disk. Basically, scenes need to be serializable.

For those of you new to the concepts of serialization and deserialization, hopefully the following explanations will make these ideas clear:

Serialization is akin to taking a snapshot of an object graph at runtime. The snapshot is written in a compact format (typically binary or SOAP) which describes the values of every object in the graph. That snapshot is put into a stream, at which point you can do anything with it as you please. Often the contents of the stream are flushed into a file and then saved to disk. Keep in mind that the object graph which was serialized continues to be used after the serialization process occurs. Serialization simply records the state of an object graph at a given point in time.

Deserialization is the process of taking the snapshot created during serialization and converting it back into “live objects” – i.e., real objects in memory that your program can use.

The next few sections of this article delve into the code required for scenes to be serialized and deserialized. Path classes require custom serialization and deserialization, which will be covered in the last two sections.

The scene serialization/deserialization services are available via static methods of the Scene class. Below is the code used to save and load a scene:

C#
public static bool Save( Scene scene, Stream stream )
{
    bool success = false;
    try
    {
        bool active = scene.IsActive;
        if( active )
            scene.Pause();
        new BinaryFormatter().Serialize( stream, scene );
        if( active )
            scene.Resume();
        success = true;
    }
    catch( Exception ex )
    {
        Debug.Fail( "Failed to save Scene.Reason: " + ex.Message );
    }
    return success;
}

public static Scene Load( Stream stream, bool pathsAreVisible )
{
    if( stream == null || ! stream.CanRead )
        throw new ArgumentException( "The stream passed" + 
                           " to Scene.Load is invalid." );

    Scene scene = null;
    try
    {
        scene = new BinaryFormatter().Deserialize( stream ) as Scene;
        foreach( Path path in scene.Paths )
            path.Visible = pathsAreVisible;
    }
    finally
    {
        if( stream != null )
            stream.Close();
    }
    return scene;
}

The Save and Load methods do not leave much to the imagination. The next few sections will examine the intricacies involved with serializing and deserializing scenes.

Using the NonSerialized Attribute

Some types can be serialized, others cannot. Some objects should be serialized, others should not. Determining which types can be serialized is easy since only types with the Serializable attribute applied to them can be serialized. Determining which objects should be serialized, on the other hand, is not always as clear cut. There are two reasons why you would intentionally exclude certain objects from the serialization process:

  • Non-serializable Types – If the type of an object in the object graph does not have the Serializable attribute applied to it, that object must be excluded from the serialization process. If the .NET serializers (namely, the BinaryFormatter and SoapFormatter) encounter a non-serializable type during the serialization process they will throw an exception. For example, the Scene class does not serialize its host since the ISceneHost interface is not serializable (in fact, interfaces can never have the Serializable attribute applied to them). This means that when a scene is deserialized, it must be given a reference to a new host.
  • Deducible Information – If a section of the object graph being serialized contains information that is easily deduced or regenerated after deserialization, then there is no reason to serialize that data. This is primarily a means of streamlining the information generated during serialization. For example, the movers on a path are not serialized since all of the information they contain can be quickly and easily regenerated after the path is deserialized. As a result of this, the memory footprint of a serialized scene can be dramatically reduced, especially if the paths in the scene contain hundreds of movers apiece.

You can inform the .NET serializers that certain fields in a type should not be serialized by applying the NonSerializedAttribute attribute to the field declaration. The following code demonstrates this:

C#
[NonSerialized]
private ISceneHost host;

The NonSerialized attribute was created to be used by the .NET serializers, but it can be used by your code as well. In the next section, we will discuss the custom serialization logic used by paths, which makes use of that attribute.

Custom Path Serialization Using Reflection

One major drawback of using the default serialization services provided by .NET serialization classes (BinaryFormatter and SoapFormatter) is that they are extremely brittle. By “brittle”, I mean that the types of the serialized objects cannot have fields added, removed, or modified for deserialization to be successful. For instance, suppose path classes used the default serialization services and that a scene containing an EllipsePath was serialized and saved to disk. If you were to then add a new field to the EllipsePath class and recompile, the saved scene could not be successfully deserialized. The .NET serializers require that the fields of a serialized object exactly match the fields of the type into which it will be deserialized. In our example above, the fact that the EllipsePath class was given a new member variable is enough to cause the deserialization process to come to a grinding halt and throw an exception.

This powerful version affinity proves quite problematic for Path-derived classes. In the Fluid Geometry animation library, paths are the hotspot for new features. Paths can be given new abilities; such as the ability to rotate, dilate, oscillate, etc. When these new features are implemented, it is necessary to add fields to the Path or Path-derived class. It would be extremely annoying to have some or all of your saved scenes become unusable just because a path was given the ability to twist or turn in some new way.

Fortunately, the wise architects of the .NET serialization infrastructure provided ample support for this type of scenario. A class can indicate to the .NET serializers that it will take care of serializing and deserializing itself, by having both the Serializable attribute applied to it and implementing the System.Runtime.Serialization.ISerializable interface. The benefit of performing the serialization/deserialization manually is that your logic can allow for new fields to exist in a class when deserializing an instance of that class. This enables us to implement new features in path classes without needing to be concerned about the ability to load previously saved scenes.

It is important to note that if you go this route, you are responsible for both the serialization and deserialization of the class instances, you cannot just do one or the other. The serialization of an object is performed in the GetObjectData method of the ISerializable interface. This interface is peculiar in that it implicitly requires the type which implements it to have a constructor with a specific signature. That constructor (which I refer to as the “deserialization constructor”) is where you perform the custom deserialization logic. The next section of this article discusses the custom deserialization of paths.

Below is the Path class’ implementation of the ISerializable.GetObjectData method, and its helper methods:

C#
public void GetObjectData( SerializationInfo info, 
                           StreamingContext context )
{
    // Cache the number of Movers on the Path.
    // This information is used during deserialization.
    //
    this.totalMoversOnPath = this.Movers.Count;

    // The serialization process is broken
    // into two phases because the reflection API
    // does not allow you to access
    // the private fields of a type's base class.
    // Since we do not want all of the Path class'
    // fields to be protected for
    // this reason, we need to serialize
    // out the Path partial first. This allows
    // the Path class to have private fields
    // that get serialized. If a new type of
    // path is created which indirectly
    // derives from Path, this logic will need to be
    // modified so that it loops over every
    // partial, not just Path and most derived type.
    //
    this.GetObjectDataHelper( info, typeof(Path) );
    this.GetObjectDataHelper( info, this.GetType() );
}

private void GetObjectDataHelper( SerializationInfo info, Type typeToProcess )
{
    BindingFlags flags = this.GetSerializationBindingFlags( typeToProcess );
    FieldInfo[] fields = typeToProcess.GetFields( flags );

    // Save the value of every non-constant field which does not 
    // have the NonSerialized attribute applied to it.
    //
    foreach( FieldInfo field in fields )
        if( ! field.IsLiteral && ! field.IsNotSerialized )
            info.AddValue( field.Name, field.GetValue( this ) );
}

private BindingFlags GetSerializationBindingFlags( Type typeToProcess )
{
    BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;

    if( typeToProcess != typeof(Path) )
        flags |= BindingFlags.DeclaredOnly;

    return flags;
}

The GetObjectData method is quite simple. It caches the number of movers on the path, which is needed after the path is deserialized. Then it makes two calls to a helper method which does the actual work of saving the state of the path into the SerializationInfo argument. SerializationInfo is basically just a key-value list which allows you to associate an object with an identifier (usually the field’s name). Later, the deserialization logic will loop over the name of every entry in the list and set the member variables to the saved values, as the next section discusses in detail.

Each class in the type hierarchy of the path is inspected individually because the reflection API does not allow you to get or set the value of private fields in a class’ base type. If the serialization was not performed in this manner, the Path class would not be able to have its private fields serialized. Considering the magnitude of the faux pas associated with non-private member variables, I just had to go the extra mile. The GetObjectDataHelper method retrieves a list of every non-public non-static field in the type currently being serialized. If the type is derived from the Path class, it uses the ‘DeclaredOnly’ flag to indicate that any protected fields inherited from the Path class should not be included in the list. The list of fields is then iterated and every non-constant field that is not decorated with the NonSerialized attribute is added to the SerializiationInfo object’s list of values. This is where the NonSerialized attribute is being consumed by code outside of the .NET serialization services, as mentioned in the previous section. The fields of Path and Path-derived classes can be decorated with the NonSerialized attribute and the custom serialization logic in the Path class will know to avoid saving their values. The FieldInfo class exposes a convenient property called IsNotSerialized which returns true if the field has the NonSerialized attribute applied to it.

There is one limitation with the current implementation of this custom serialization logic. A Path-derived class cannot have a field with the same name as a field in the Path class. This is due to the fact that only the name of the field is stored in the SerializationInfo object. If the Path class has a private field named ‘foo’ and a Path-derived class declared a field named ‘foo’ as well, there would be no way for the deserialization logic to know whose ‘foo’ is whose. I’m not worried about this. If this did become a problem then the solution is to prepend the name of the declaring class followed by a separator before the field name, such as “Path+foo” for the Path’s ‘foo’ field. That would serve as the key for the Path’s ‘foo’ value in the SerializationInfo object. The deserialization logic would need to split the key on the separator and then it would have enough information to be able to set the variable on the correct class.

One final point to note is that the serialization logic seen above is in the abstract Path class. It is also necessary for Path-derived classes to be decorated with the Serializable attribute and implement ISerializable. The .NET serializers require that the runtime type of an object to be serialized explicitly indicates that it is serializable. Implementing this on a Path-derived class is simple enough:

C#
[Serializable]
public class SpiralPath : Path, ISerializable
{
    // Other members omitted...

    void ISerializable.GetObjectData( SerializationInfo info, 
                                      StreamingContext context )
    {
        base.GetObjectData( info, context );
    }

    protected SpiralPath( SerializationInfo info, 
                          StreamingContext context )
        : base( info, context )
    {
    }
}

Now that we’ve seen how a scene and its paths are serialized, it’s time to turn our attention to the other side of the coin. The next section examines the code required to deserialize a scene, including the complicated task of performing custom deserialization for paths.

Deserializing Complex Object Graphs

Deserializing scenes would be very simple if Paths did not use custom serialization. It would just be a matter of calling the Deserialize method on a BinaryFormatter, as seen in the Load method shown at the beginning of this topic. However, since Path and Path-derived classes implement the ISerializable interface, which requires them to handle their own serialization and deserialization, the situation becomes more complicated than just a single method call.

In the previous section, it was mentioned that implementing the ISerializable interface implies that a special constructor must be implemented, which I refer to as a “deserialization constructor”. You will not receive a compiler error if the type which implements ISerializable does not have a deserialization constructor, but you will receive an exception at runtime when you attempt to deserialize an object of that type. This peculiar situation can be attributed to the fact that interface definitions cannot include constructors or non-public members, yet to avoid security and versioning problems, the method used for deserialization has to be a non-public constructor. The .NET Framework documentation contains more information about this topic here.

The deserialization constructor of the Path class provides the deserialization logic used by all path types. It receives a SerializationInfo object containing the values stored during the serialization process. Reflection is used to set the value of the Path’s member variables to the saved values found in the SerializationInfo argument. The Path class’ deserialization constructor and its helper methods are listed below:

C#
protected Path( SerializationInfo info, StreamingContext context )
{
    // Refer to GetObjectData for an explanation
    // of why this is performed in multiple steps.
    //
    this.DeserializationCtorHelper( info, typeof(Path) );
    this.DeserializationCtorHelper( info, this.GetType() );
}

private void DeserializationCtorHelper( SerializationInfo info, 
                                        Type typeToProcess )
{
    BindingFlags flags = this.GetSerializationBindingFlags( typeToProcess );
    foreach( SerializationEntry entry in info )
    {
        FieldInfo field = typeToProcess.GetField( entry.Name, flags );
        if( field != null )
            field.SetValue( this, entry.Value );
    }
}

private BindingFlags GetSerializationBindingFlags( Type typeToProcess )
{
    BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;

    if( typeToProcess != typeof(Path) )
        flags |= BindingFlags.DeclaredOnly;

    return flags;
}

The deserialization logic is extremely similar to the serialization logic seen in the previous section. Each type in the Path class hierarchy is inspected individually because the reflection API does not allow you to gain access to the private members of a class’ base type. Unlike the serialization logic, while deserializing, it is not necessary to check if the field being set is constant or has the NonSerialized attribute applied to it because only non-constant serializable values are saved during the serialization process.

Another complicating factor is that the movers on a path are not serialized, which was discussed in the first section of this topic. As a result of that optimization, it is necessary to add the appropriate number of movers back into the Path’s MoverCollection during the deserialization process. The movers could be added back in by the deserialization constructor, but I chose to make use of the IDeserializationCallback interface to handle this task instead. IDeserializationCallback is an interface provided by the .NET Framework which allows you to be notified when an object has finished being deserialized. The interface contains one method: OnDeserialization. The Path class implements this interface and adds the movers back into the MoverCollection in that method. Below is the Path’s implementation of IDeserializationCallback:

C#
void IDeserializationCallback.OnDeserialization( object sender )
{
    // Since this.movers is not serialized, we need to add the Movers
    // back into the Path once deserialization is complete.Not serializing
    // the Movers reduces the memory footprint of a serialized Scene.
    //
    for( int i = 0; i < this.totalMoversOnPath; ++i )
        this.Movers.Add( new Mover( this ) );
}

I chose to use this interface, as opposed to putting the above code in the deserialization constructor, because this task involves more than just restoring the state of simple member variables. The documentation for IDeserializationCallback states: “If an object needs to execute code on its child objects, it can delay this action, implement IDeserializationCallback, and execute the code only when it is called back on this interface.” When the OnDeserialization method is invoked, you can be sure that the object has been fully deserialized and is in a valid state.

This topic explained when and why it is worthwhile to implement custom serialization for a class. It is not necessary to use reflection to perform these tasks, but doing so prevents you from needing to update the serialization logic when you modify the set of fields belonging to the class. Also, using this reflection-based approach could prove beneficial in situations where the class which supports custom serialization is being maintained by less experienced developers who might not be aware of the custom serialization logic.

Custom Double Buffered Rendering Using GDI+

After reading this topic, you will know:

  • How and why to use double buffered rendering.
  • How and why Fluid Geometry implements custom double buffered rendering.

Many WinForms applications have no need to perform much or any custom drawing. Typically, all of the rendering involved in an application’s user interface is taken care of by controls on Forms. This holds true especially for business applications, where the primary focus of the user interface is on the creation, modification, transmission, analysis and deletion of business data. Applications of that nature usually do not need to draw many dashed lines, blue rectangles, etc. In some situations, however, it becomes necessary for an application to perform custom rendering. If the custom rendering performed by an application needs to be updated often, double buffering can be used to reduce flickering.

For those of you new to the concept of double buffering, I hope that the following explanation will clarify it for you:

Double Buffering is a drawing technique used to help reduce the amount of flickering caused by frequent paint operations. Rather than an application drawing directly to the screen, it first draws to an in-memory buffer. Once the entire image has been rendered, the buffer is then copied to the screen in one operation. Since drawing directly to the screen is very slow, it is better to do as much drawing as possible in memory and then just copy that buffered image directly to the screen. This is particularly beneficial in situations where the image being rendered is a composite of many overlapping visual elements, because drawing one layer of an image at a time to the screen increases the total number of drawing operations performed by the screen. The more drawing operations performed by the screen, the more flickering can be observed. It is important to note that double buffering does not necessarily make the rendering process any faster, actually it can make it slower. The impetus, or raison d'être, of double buffered rendering is simply to reduce the flickering caused by frequent paint operations.

The .NET Framework provides support for double buffering via the protected SetStyle method of the Control class. Since the Form class indirectly derives from Control it can also use the SetStyle method to enable double buffered rendering for itself. When you enable double buffering on a Form (or any control), the Graphics object passed into its OnPaint method and Paint event handler(s) will actually draw onto an in-memory buffer. There are many great articles on the Web which discuss how to use the standard .NET double buffering technique, so we will not go any further into it here.

Fluid Geometry absolutely requires double buffering, but it cannot rely on the standard double buffering provided by the .NET Framework. It needs to be double buffered because scenes repaint themselves hundreds of times per second. It cannot use the standard .NET double buffering because a scene has no idea who will be hosting it, as the next topic covers in great detail. Since a scene does not know who will be hosting it, there is no way for it to be sure that its host supports double buffering. This requires the animation library to implement custom double buffering which, as a side benefit, frees a scene host from ever needing to explicitly support double buffering. Actually, if a scene host were to support double buffering, it would need to jump through hoops to ensure that the scene renders into the correct Graphics object.

The following diagram illustrates the basic idea of how double buffering works in the animation library:

Image 8

A scene has an off-screen buffer into which the movers in the scene render. After all of the paths have finished rendering their movers, the buffer is copied to a Graphics object provided by the scene’s host. That Graphics object then draws the newly generated image to the screen in an atomic operation.

Below is the logic in the Scene class which provides a rendering surface for the movers:

C#
private Bitmap RenderingSurface
{
    get
    {
        if( this.renderingSurface == null )
            this.renderingSurface = new Bitmap( 
                Math.Max( this.Size.Width,  1 ),
                Math.Max( this.Size.Height, 1 ) );

        return this.renderingSurface;
    }
}

internal Graphics OffscreenGraphics
{
    get
    {
        if( this.grfxRenderingSurface == null )
            this.grfxRenderingSurface = 
                    Graphics.FromImage( this.RenderingSurface );

        return this.grfxRenderingSurface;
    }
}

private void OnHostResize(object sender, EventArgs e)
{
    bool wasActive = this.IsActive;
    this.Stop( true );

    if( this.renderingSurface != null )
    {
        this.renderingSurface.Dispose();
        this.renderingSurface = null;
    }

    if( this.grfxRenderingSurface != null )
    {
        this.grfxRenderingSurface.Dispose();
        this.grfxRenderingSurface = null;
    }

    foreach( Path path in this.Paths )
        path.Reinitialize();

    if( wasActive )
        this.Start();
}

The code seen above creates a buffer (a System.Drawing.Bitmap object) which has the same size as the display area provided by the scene’s host. The movers never directly reference that Bitmap, instead, they draw onto it via the OffscreenGraphics property. That property simply caches a Graphics object which can draw onto the buffer and returns a reference to it. The OnHostResize event handler is called when the host is resized to ensure that the dimensions of the off-screen buffer are the same as that of the actual display area made available by the scene’s host.

When it is time to copy the buffer to the screen, the following method in the Scene class is invoked:

C#
internal void UpdateView()
{
    if( this.host == null || this.isDisposed )
        return;

    try
    {
        this.host.SceneGraphics.DrawImageUnscaled( this.RenderingSurface, 
                                                   Point.Empty );
    }
    catch( Exception ex )
    {
        // In case something goes wrong, stop the Scene.
        //
        this.Stop( false );
    }
}

The buffer is copied to the screen via the DrawImageUnscaled method because it is very fast and the size of the buffer is the same as the size of the display area. The Point.Empty argument specifies the upper-left coordinate of where the image will be drawn on the screen (Point.Empty means the (0, 0) coordinate).

The Mover class draws onto the off-screen buffer, as shown below:

C#
protected virtual void Draw( bool visible )
{
    if( ! this.Scene.IsActive || ! this.IsInView )
        return;

    if( visible )
        this.Scene.OffscreenGraphics.DrawImage( this.Path.MoverImage, 
                                                this.Bounds );
    else
        this.Scene.OffscreenGraphics.FillRectangle( this.Scene.BackgroundBrush, 
                                                    this.EraseBounds );
}

Movers use the DrawImage method instead of DrawImageUnscaled for a couple of reasons. One reason is the fact that DrawImageUnscaled does not accept floating point coordinates, which is necessary for the exact locations of movers on a path to be preserved. Also, if movers on a path are displaying a user-supplied image, then that image must be scaled because the size of the image might not be the same as the size of the movers displaying it. Scaling an image ensures that it will occupy all of the space into which it is rendered (think of it as “stretching” the image out).

The MovementManager class is the heart of the rendering engine. We will not be looking at how it works in detail here, but the following method from that class shows how the entire rendering process is tied together:

C#
private void OnTimerTick(object sender, EventArgs e)
{
    ++this.tickCount;

    if( this.ShouldCreateMotion )
    {
        PathCollection visiblePaths = this.Scene.VisiblePaths;

        foreach( Path path in visiblePaths )
            if( this.PathNeedsToMove( path ) )
                path.Erase();

        foreach( Path path in visiblePaths )
            if( this.PathNeedsToMove( path ) )
                path.Move();

        // Draw every visible path whether it was moved or not
        // because the erasing performed by moved Paths will
        // leave "holes" in paths lower in the Z order.
        foreach( Path path in visiblePaths )
            path.Draw();

        this.Scene.UpdateView();
    }
}

This method responds to the Tick event of a Timer, which happens to fire once every millisecond. If it determines that at least one path in the scene needs to move, it tells the appropriate paths to erase, move, and draw. When a path is told to erase, move, or draw itself, it simply tells every mover it contains to perform that same action. After the paths have been updated and the scene’s off-screen buffer contains a new image, the scene’s UpdateView method is called so that the new image is copied to the screen.

This topic discussed the concept of double buffering and when it should be used. For most applications that would benefit from the use of double buffered rendering, it is sufficient to use the .NET Framework’s built-in double buffering support. Considering the graphics-intensive nature of Fluid Geometry and the fact that the rendering logic is exposed as a hosted service, it is necessary for Fluid Geometry to use custom double buffering. The next topic examines why and how the Scene class is hosted.

Decoupling a Scene from its Host (ISceneHost)

After reading this topic, you will know:

  • The benefit of loosely coupling reusable types to the consumers of those types.
  • How to use interfaces to loosely couple distinct portions of an application.

The Fluid Geometry animation library is able to display scenes through any object, provided that the object implements the ISceneHost interface. The object which implements the ISceneHost interface is referred to as the scene’s “host”. A host provides several pieces of information needed for its scene to operate. A Scene does not care what object is hosting it, just as long as it implements ISceneHost. The benefit to this technique is that any application can display scenes on any rendering surface it sees fit. For example, the Fluid Geometry configuration application displays scenes in two different places. The “Path Preview” GroupBox contains a preview of the selected path, and a separate window is launched which displays a scene when the “View Scene” button is clicked. The ConfigurationForm implements ISceneHost for the preview path to be displayed. The SceneForm, which is actually in the animation library assembly, implements ISceneHost as well.

Below is the declaration of the ISceneHost interface:

C#
/// <summary>
/// ISceneHost provides services needed by a Scene.
/// </summary>
public interface ISceneHost
{
    /// <summary>
    /// Returns the Graphics object to be used
    /// when rendering into the scene. Do NOT dispose of this object.
    /// </summary>
    System.Drawing.Graphics SceneGraphics { get; }

    /// <summary>
    /// Gets the size of the visual area available for the scene.
    /// </summary>
    System.Drawing.Size Size { get; }

    /// <summary>
    /// The color of the scene's background.
    /// </summary>
    System.Drawing.Color BackColor { get; }

    /// <summary>
    /// Fires when the scene needs to be repainted.
    /// </summary>
    event System.Windows.Forms.PaintEventHandler Paint;

    /// <summary>
    /// Fires when the size of the scene changes.
    /// </summary>
    event EventHandler Resize;
}

The two most important members of the interface are SceneGraphics and Size. A scene draws to the Graphics object exposed through the SceneGraphics property. The Size property is used to determine the physical dimensions of the scene.

The following is the ConfigurationForm’s implementation of the ISceneHost interface. Keep in mind that the ‘pnlSceneHost’ member variable is the Panel control onto which the preview path is rendered.

C#
Color ISceneHost.BackColor
{
    get { return this.pnlSceneHost.BackColor; }
}

Graphics ISceneHost.SceneGraphics
{
    get
    {
        if( this.grfxSceneHost == null )
            this.grfxSceneHost = this.pnlSceneHost.CreateGraphics();

        return this.grfxSceneHost;
    }
}

Size ISceneHost.Size
{
    get { return this.pnlSceneHost.Size; }
}

It is important to note that if a Form-derived class is implementing this interface, you need to use explicit interface implementation to avoid name clashes with the existing properties with the same names, such as BackColor and Size. Since the ConfigurationForm inherits the Paint and Resize events, there is no need to implement them again. One point to notice is that the SceneGraphics property caches a reference to the Graphics object used to render onto the Panel. That is an important optimization to make considering the frequency that the scene accesses it.

When a Scene is instantiated (or deserialized), it must be given a host before it can start animating. The Scene class exposes a public method for this purpose:

C#
public void AttachHost( ISceneHost host )
{
    if( host == null )
        throw new ArgumentNullException( "host", 
                  "The Scene's host cannot be null." );

    if( this.host != null )
        this.DetachHost();
    this.host = host;
    this.host.Paint += 
         new System.Windows.Forms.PaintEventHandler( this.RepaintScene );
    this.host.Resize += new EventHandler( this.OnHostResize );
}

If the scene’s host needs to be removed or changed, the DetachHost method can be used:

C#
public ISceneHost DetachHost()
{
    if( this.host == null )
        return null;

    ISceneHost sceneHost = this.host;
    this.host.Paint -= 
         new System.Windows.Forms.PaintEventHandler( this.RepaintScene );
    this.host.Resize -= new EventHandler( this.OnHostResize );
    this.host = null;
    return sceneHost;
}

This topic discussed the advantage of decoupling a scene from its host. If the Fluid Geometry animation library was not intended to be reusable then it would not have been necessary to generalize the relationship between a scene and the surface upon which it renders. The Scene class could have simply taken in a Panel control as its host. However, since the animation library should be reusable in a wide variety of contexts, it was necessary to abstract the relationship between a scene and its host into a formal contract, which can be implemented as the host sees fit.

Conclusion

The topics covered in this article constitute only a handful of the challenges overcome while implementing Fluid Geometry. I hope that the topics examined prove useful and/or interesting for you. If you decide to explore the source code and come up with a question, comment, suggestion, etc., please feel free to post your thoughts to the message board associated with this article. I’ll do my best to provide a timely response, if necessary. Thank you for taking the time to read my article, I hope it was worthwhile. J

History

  • 13th September, 2005: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)