Click here to Skip to main content
15,897,371 members
Articles / Desktop Programming / WPF

A (Mostly) Declarative Framework for Building Simple WPF-based Wizards

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
7 Mar 2011LGPL322 min read 19.3K   229   15  
A declarative framework for building WPF wizards.
/*
* Olbert.Utilities.nHydrate
* utilities for simplifying the use of nHydrate entities in WPF applications
* Copyright (C) 2011  Mark A. Olbert
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published 
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.ObjectModel;
using Olbert.Utilities;
using System.Data.Objects;
using System.Runtime.Serialization;
using System.Configuration;
using System.Xml.Serialization;
using System.Xml.Linq;
using System.Reflection;
using System.Xml;

namespace Olbert.nHydrate
{
    /// <summary>
    /// Defines a class which holds information about an individual nHydrate EFDAL "connection", i.e., the
    /// information needed to define an EntityConnectionString. Implemented as a KeyedCollection of
    /// DatabaseConnectionInfo objects, each of which defines the Sql connection parameters for a database
    /// that will work with the required nHydrate schema and version
    /// </summary>
    [Serializable]
    public class nHydrateConnection : ObservableCollection<DatabaseConnectionInfo>,
        IXmlSerializable, IDatabaseInfoCollection
    {
        private string name;
        private Type entityContextType;
        private string provider;
        private Guid schema;
        private nHydrateVersion version;
        private int defaultDBIndex = -1;

        /// <summary>
        /// Initializes an instance from the supplied values and sets Provider to its 
        /// default value, System.Data.SqlClient
        /// </summary>
        /// <param name="name">the name of the instance being created. Null or empty value will
        /// cause an exception</param>
        /// <param name="entityContextType">the Type of EntityContext/ObjectContext being described.
        /// Types other than ObjectContext itself or a class derived from ObjectContext will cause
        /// an exception. A Type that does not have a public constructor taking a single
        /// string argument (i.e., an entity connection string) will also cause an exception.</param>
        /// <param name="schema">the nHydrate schema. Empty or null values will cause an exception</param>
        /// <param name="version">the nHydrate schmea version. Undefined values will cause an
        /// exception</param>
        public nHydrateConnection( string name, Type entityContextType, Guid schema, nHydrateVersion version )
            : this()
        {
            Name = name;
            EntityContextType = entityContextType;
            Schema = schema;
            Version = version;
        }

        /// <summary>
        /// Initializes an instance and sets Provider to its default value, System.Data.SqlClient. This
        /// variant is intended to be called by the Xml configuration file parser
        /// </summary>
        protected internal nHydrateConnection()
        {
            Provider = "System.Data.SqlClient";
        }

        /// <summary>
        /// The name of the DatabaseConnection (which is also the key). Cannot be null or
        /// empty. Empty or null values throw an exception.
        /// </summary>
        public string Name 
        {
            get { return name; }

            protected set
            {
                if( ( value == null ) || String.IsNullOrEmpty(value.Trim()) )
                    throw new TracedException("Supplied nHydrateConnection Name is undefined or empty");

                name = value;
            }
        }

        /// <summary>
        /// Gets or sets the index in the collection of the default DatabaseConnectionInfo object.
        /// Values &lt; -1 or out of bounds are coerced to -1, which is the "no default exists" value
        /// </summary>
        public int DefaultDatabaseIndex
        {
            get { return defaultDBIndex; }

            set
            {
                if( value < -1 ) value = -1;

                if( value >= Count ) value = -1;

                defaultDBIndex = value;
            }
        }

        /// <summary>
        /// Gets the default DatabaseConnectionInfo object if one is defined, returning null
        /// if it isn't
        /// </summary>
        public DatabaseConnectionInfo DefaultDatabase
        {
            get
            {
                if( ( DefaultDatabaseIndex < 0 ) || ( DefaultDatabaseIndex >= Count ) ) return null;

                return this[DefaultDatabaseIndex];
            }
        }

        /// <summary>
        /// Gets or sets the Type of the entity context object being represented by this instance.
        /// Null values, or values that are neither ObjectContext nor descended from ObjectContext
        /// will cause an exception to be thrown.
        /// <para>The provided Type must also have a public constructor that takes a single string
        /// parameter. The nHydrate code generator produces such a variant, which can be used to 
        /// create an entity context instance from a particular connection string. That 
        /// functionality is required for this nHydrate utility framework to work.</para>
        /// </summary>
        public Type EntityContextType
        {
            get { return entityContextType; }

            set
            {
                if( value == null )
                    throw new TracedException("Supplied EntityContext Type was undefined");

                if( !( value == typeof(ObjectContext) )
                    && !value.IsSubclassOf(typeof(ObjectContext)) )
                    throw new TracedException("Supplied EntityContext Type was not an ObjectContext or derived from ObjectContext");

                ConstructorInfo ctor = value.GetConstructor(new Type[] { typeof(string) });

                if( ctor == null )
                    throw new TracedException("Supplied EntityContext Type does not have a public constructor taking a single string argument");

                entityContextType = value;
                EntityConstructor = ctor;
            }
        }

        /// <summary>
        /// Gets or sets the database provider used by the Entity Framework. Null or empty values are
        /// not allowed, and will produce an exception. Default is System.Data.SqlClient.
        /// </summary>
        public string Provider
        {
            get { return provider; }

            set 
            {
                if( ( value == null ) || String.IsNullOrEmpty(value.Trim()) )
                    throw new TracedException("Supplied Provider value was undefined or empty");

                provider = value;
            }
        }

        /// <summary>
        /// Gets or sets the nHydrate schema that all the database connections defined by this instance
        /// must have
        /// </summary>
        public Guid Schema
        {
            get { return schema; }

            protected set
            {
                if( ( value == null ) || ( value == Guid.Empty ) )
                    throw new TracedException("Supplied nHydrate schema is undefined or empty");

                schema = value;
            }
        }

        /// <summary>
        /// Gets or sets the version of the nHydrate schema that all the database connections defined by
        /// this instance must have
        /// </summary>
        public nHydrateVersion Version 
        {
            get { return version; }

            protected set
            {
                if( ( value == null ) || value.IsFirstUpgrade || value.IsUndefined )
                    throw new TracedException("Supplied nHydrate version is undefined");

                version = value;
            }
        }

        /// <summary>
        /// Gets an enumerator which returns all of the databases contained in this instance which are 
        /// compatible with it.
        /// <para>Because this method accesses each SqlServer database it can take a long time to
        /// complete if there is a problem connecting to a database.</para>
        /// </summary>
        /// <param name="a">the Rijndael salt used to encrypt user credentials</param>
        /// <param name="b">the Rijndal key used to encrypt user credentials</param>
        /// <returns>an enumerator which returns all of the databases contained in this instance which are 
        /// compatible with it</returns>
        public IEnumerator<DatabaseConnectionInfo> GetCompatibleDatabases( string a, string b )
        {
            foreach( DatabaseConnectionInfo curDCI in this )
            {
                if( curDCI.IsCompatibleWithSchemaAndVersion(a, b) )
                    yield return curDCI;
            }

            yield break;
        }

        /// <summary>
        /// Deletes any DatabaseConnectionInfo objects in this instance which are not compatible with
        /// the instance's nHydrate schema and version.
        /// <para>Because this method accesses each SqlServer database it can take a long time to
        /// complete if there is a problem connecting to a database.</para>
        /// <para>Be careful using this method. If you pass it the wrong Rijndael salt and key it will
        /// delete every DatabaseConnectionInfo object which does not use integrated security (i.e.,
        /// Windows identity based authentication) for access.</para>
        /// </summary>
        /// <param name="a">the Rijndael salt used to encrypt user credentials</param>
        /// <param name="b">the Rijndal key used to encrypt user credentials</param>
        public void RemoveIncompatibleDatabases( string a, string b )
        {
            List<int> toRemove = new List<int>();

            for( int idx = 0; idx < Count; idx++ )
            {
                if( !this[idx].IsCompatibleWithSchemaAndVersion(a, b) )
                    toRemove.Add(idx);
            }

            for( int idx = toRemove.Count - 1; idx >= 0; idx-- )
            {
                this.RemoveAt(toRemove[idx]);
            }
        }

        /// <summary>
        /// Removes the DatabaseConnectionInfo object with the supplied Name, if it
        /// exists in the collection
        /// </summary>
        /// <param name="dbConnectionName">the Name of the DatabaseConnectionInfo object
        /// to be deleted</param>
        /// <returns>true if the item was found and deleted, false otherwise</returns>
        public bool Remove( string dbConnectionName )
        {
            int toRemove = -1;

            for( int idx = 0; idx < Count; idx++ )
            {
                if( this[idx].Name == dbConnectionName )
                {
                    toRemove = idx;
                    break;
                }
            }

            if( toRemove < 0 ) return false;

            RemoveAt(toRemove);

            return true;
        }

        /// <summary>
        /// Gets the EntityContext's constructor which takes a single string argument, an
        /// EntityConnectionString
        /// </summary>
        public ConstructorInfo EntityConstructor { get; protected set; }

        /// <summary>
        /// Part of the IXmlSerializable interface. This implementation always returns null;
        /// <para>Implementing the IXmlSerializable interface is difficult. I am indebted to
        /// Jaap de Haan's article on CodeProject, which explained the process in great detail,
        /// and which shows how to work around the numerous gotchas. You can read Jaap's 
        /// article at http://www.codeproject.com/KB/XML/ImplementIXmlSerializable.aspx.</para>
        /// </summary>
        /// <returns>null</returns>
        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

        /// <summary>
        /// Part of the IXmlSerializable interface. This implementation reads the Xml sourced from
        /// the configuration file and parses it into an nHydrateConnection object. It would be quite
        /// odd to call this method from your own code. It is intended to be called by the .NET
        /// Framework's configuration subsystem.
        /// <para>Implementing the IXmlSerializable interface is difficult. I am indebted to
        /// Jaap de Haan's article on CodeProject, which explained the process in great detail,
        /// and which shows how to work around the numerous gotchas. You can read Jaap's 
        /// article at http://www.codeproject.com/KB/XML/ImplementIXmlSerializable.aspx.</para>
        /// </summary>
        /// <param name="reader">An XmlReader provided by the .NET Framework configuration subsystem</param>
        public void ReadXml( System.Xml.XmlReader reader )
        {
            // unfortunately, you can't use linq to xml here, apparently because the reader
            // isn't starting at the beginning of the document or something. so we have to
            // do this the old-fashioned way...
            //reader.MoveToContent();
            
            Name = reader.GetAttribute("Name");
            EntityContextType = Type.GetType(reader.GetAttribute("EntityContextType"));

            // read, but do not set, the default database index until after we
            // load all the DatabaseConnectionInfo objects. This is necessary because
            // otherwise the default index will get "bumped" when the first DatabaseConnectionInfo
            // object is added, since its index, 0, will always be less than or equal to
            // any default index value
            int defaultDB = -1;
            int.TryParse(reader.GetAttribute("DefaultDatabaseIndex"), out defaultDB);

            string temp = reader.GetAttribute("Provider");
            if( ( temp != null ) && !String.IsNullOrEmpty(temp.Trim()) )
                Provider = temp;

            Guid tempGuid = Guid.Empty;
            Guid.TryParse(reader.GetAttribute("Schema"), out tempGuid);
            Schema = tempGuid;

            Version = nHydrateVersion.Parse(reader.GetAttribute("Version"));

            // we need to consume the element or the deserialization routine will call 
            // ReadXml endlessly
            bool emptyElement = reader.IsEmptyElement;

            reader.ReadStartElement();

            if( !emptyElement )
            {
                int numConn = 0;
                int.TryParse(reader.GetAttribute("Count"), out numConn);

                for( int idx = 0; idx < numConn; idx++ )
                {
                    DatabaseConnectionInfo dbci = new DatabaseConnectionInfo(this);

                    reader.ReadStartElement();
                    dbci.ReadXml(reader);

                    this.Add(dbci);
                }
            }

            // now we can finally set whatever default database index was stored
            DefaultDatabaseIndex = defaultDB;
        }

        /// <summary>
        /// Part of the IXmlSerializable interface. This implementation writes an Xml representation
        /// of the instance to the supplied XmlWriter. It would be quite
        /// odd to call this method from your own code. It is intended to be called by the .NET
        /// Framework's configuration subsystem.
        /// <para>Implementing the IXmlSerializable interface is difficult. I am indebted to
        /// Jaap de Haan's article on CodeProject, which explained the process in great detail,
        /// and which shows how to work around the numerous gotchas. You can read Jaap's 
        /// article at http://www.codeproject.com/KB/XML/ImplementIXmlSerializable.aspx.</para>
        /// </summary>
        /// <param name="writer">An XmlWriter provided by the .NET Framework configuration subsystem</param>
        public void WriteXml( System.Xml.XmlWriter writer )
        {
            writer.WriteAttributeString("Name", Name);
            writer.WriteAttributeString("EntityContextType", EntityContextType.AssemblyQualifiedName);

            if( DefaultDatabaseIndex >= 0 )
                writer.WriteAttributeString("DefaultDatabaseIndex", DefaultDatabaseIndex.ToString());
            
            if( (Schema != null) && (Schema != Guid.Empty) )
                writer.WriteAttributeString("Schema", Schema.ToString());

            if( ( Version != null ) && !Version.IsUndefined && !Version.IsFirstUpgrade )
                writer.WriteAttributeString("Version", Version.ToString());

            if( Count > 0 )
            {
                writer.WriteStartElement("ArrayOfDatabaseConnectionInfo");
                writer.WriteAttributeString("Count", Count.ToString());

                foreach( DatabaseConnectionInfo curDCI in this )
                {
                    writer.WriteStartElement("DatabaseConnectionInfo");

                    curDCI.WriteXml(writer);

                    writer.WriteEndElement();
                }

                writer.WriteEndElement();
            }
        }

        /// <summary>
        /// Overrides the base implementation to remove the association between the items
        /// being removed and this object (i.e., sets the List property of each
        /// DatabaseConnectionInfo object to null)
        /// </summary>
        protected override void ClearItems()
        {
            // remove the association with this object
            foreach( DatabaseConnectionInfo curDBCI in this )
            {
                curDBCI.List = null;
            }

            DefaultDatabaseIndex = -1;

            base.ClearItems();
        }

        /// <summary>
        /// Overrides the base implementation to associate the item being added with
        /// this object (i.e., sets the List property of the item being inserted to
        /// this instance)
        /// </summary>
        /// <param name="index">the location in the collection where the object is being inserted</param>
        /// <param name="item">the DatabaseConnectionInfo item being inserted</param>
        protected override void InsertItem( int index, DatabaseConnectionInfo item )
        {
            item.List = this;

            base.InsertItem(index, item);

            if( index <= DefaultDatabaseIndex ) DefaultDatabaseIndex++;
        }

        /// <summary>
        /// Overrides the base implementation to remove the association between the item
        /// being removed and this object (i.e., sets the List property of the
        /// DatabaseConnectionInfo object to null)
        /// <para>The DefaultDatabaseIndex is cleared if the deleted item is the
        /// existing default database connection.</para>
        /// </summary>
        /// <param name="index">the location in the collection of the object being removed</param>
        protected override void RemoveItem( int index )
        {
            this[index].List = null;

            if( index == DefaultDatabaseIndex ) DefaultDatabaseIndex = -1;

            base.RemoveItem(index);
        }

        /// <summary>
        /// Overrides the base implementation to associate the item being added with
        /// this object (i.e., sets the List property of the item being inserted to
        /// this instance) and to disassociate the item being overwritten.
        /// <para>The DefaultDatabaseIndex is cleared if the new item is overwriting the
        /// existing default database connection.</para>
        /// </summary>
        /// <param name="index">the location in the collection where the object is being set</param>
        /// <param name="item">the DatabaseConnectionInfo item being inserted</param>
        protected override void SetItem( int index, DatabaseConnectionInfo item )
        {
            if( index == DefaultDatabaseIndex ) DefaultDatabaseIndex = -1;
            else
            {
                if( index <= DefaultDatabaseIndex ) DefaultDatabaseIndex++;
            }

            this[index].List = null;
            item.List = this;

            base.SetItem(index, item);
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Jump for Joy Software
United States United States
Some people like to do crossword puzzles to hone their problem-solving skills. Me, I like to write software for the same reason.

A few years back I passed my 50th anniversary of programming. I believe that means it's officially more than a hobby or pastime. In fact, it may qualify as an addiction Smile | :) .

I mostly work in C# and Windows. But I also play around with Linux (mostly Debian on Raspberry Pis) and Python.

Comments and Discussions