Archive | March, 2009

A better WF Rule Serializer

The Serialaizer supplied with the Microsoft rules engine stores the CodeDom expressions into the XML.
This is less than readable (can you imaging performing differences on this in version control – and you do keep your rules in version control don’t you?) and very wordy (100 lines per line of rule text).

Here is a greatly improved version.  It stores the rule in a format as close to that the user entered.  It parses the code at serialization time.  It uses the same code as the rule engine to perform the parsing, requiring reflection to get at the code…

=== TextRuleSetSerializer ===

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Workflow.Activities.Rules;
using System.Xml;

namespace PerfectStorm.Rules
{
    /// <summary>
    /// This is a far more compact serializer than the one supplied with the WF rules engine.
    /// </summary>
    public class TextRuleSetSerializer
    {
        /// <summary>
        /// This is a proxy to the internal Parser class in the System.Workflow.Activities assembly.
        /// </summary>
        private class ParserProxy
        {
            private object _parser;
            private Type _parserType;

            /// <summary>
            ///
            /// </summary>
            /// <param name=”factType”></param>
            public ParserProxy(Type factType)
            {
                //This is a bit dangerous – it may require a rewrite with each new version of the framework.
                Assembly a = Assembly.Load(“System.Workflow.Activities, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35″);
                _parserType = a.GetType(“System.Workflow.Activities.Rules.Parser”);
                Type[] constructorParam = { typeof(RuleValidation) };
                RuleValidation validation = new RuleValidation(factType, null);
                object[] callParams = { validation };
                ConstructorInfo ci = _parserType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance,
                    null, constructorParam, null);
                _parser = ci.Invoke(callParams);
            }

            /// <summary>
            ///
            /// </summary>
            /// <param name=”fragment”></param>
            /// <returns></returns>
            public RuleExpressionCondition ParseCondition(string fragment)
            {
                object[] parserParameters = { fragment };
                return (RuleExpressionCondition)_parserType.InvokeMember(“ParseCondition”,
                    BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
                    null, _parser, parserParameters);
            }

            /// <summary>
            ///
            /// </summary>
            /// <param name=”statements”></param>
            /// <returns></returns>
            public List<RuleAction> ParseStatementList(string statements)
            {
                object[] parserParameters = { statements };
                return (List<RuleAction>)_parserType.InvokeMember(“ParseStatementList”,
                    BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
                    null, _parser, parserParameters);
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name=”writer”></param>
        /// <param name=”rs”></param>
        public void Serialize(XmlWriter writer, RuleSet rs)
        {
            writer.WriteStartDocument();
            writer.WriteStartElement(“RuleSet.Text”);
            writer.WriteAttributeString(“Name”, rs.Name);
            writer.WriteAttributeString(“Description”, rs.Description);
            writer.WriteAttributeString(“ChainingBehavior”, rs.ChainingBehavior.ToString());

            foreach (var rule in rs.Rules)
            {
                writer.WriteStartElement(“Rule”);
                writer.WriteAttributeString(“Active”, rule.Active.ToString());
                writer.WriteAttributeString(“Description”, rule.Description);
                writer.WriteAttributeString(“Name”, rule.Name);
                writer.WriteAttributeString(“Priority”, rule.Priority.ToString());
                writer.WriteAttributeString(“ReevaluationBehavior”, rule.ReevaluationBehavior.ToString());

                writer.WriteStartElement(“Condition”);
                writer.WriteValue(rule.Condition.ToString());
                writer.WriteEndElement();

                writer.WriteStartElement(“ThenActions”);

                foreach (var action in rule.ThenActions)
                {
                    writer.WriteStartElement(“Action”);
                    writer.WriteValue(action.ToString());
                    writer.WriteEndElement();
                }

                writer.WriteEndElement();

                writer.WriteStartElement(“ElseActions”);

                foreach (var action in rule.ElseActions)
                {
                    writer.WriteStartElement(“Action”);
                    writer.WriteValue(action.ToString());
                    writer.WriteEndElement();
                }

                writer.WriteEndElement();
                writer.WriteEndElement();
            }

            writer.Flush();
        }

        /// <summary>
        ///
        /// </summary>
        /// <typeparam name=”T”></typeparam>
        /// <param name=”value”></param>
        /// <returns></returns>
        private T GetEnumValue<T>(string value)
        {
            if (!typeof(T).IsSubclassOf(typeof(Enum)))
                throw new Exception(“Must be an Enum”);

            int[] allValues = (int[])Enum.GetValues(typeof(T));

            foreach (int i in allValues)
            {
                T newValue = (T)Enum.ToObject(typeof(T), i);
                if (newValue.ToString() == value)
                {
                    return newValue;
                }
            }
            throw new Exception(value + ” is not of Type ” + typeof(T).Name);
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name=”factType”></param>
        /// <param name=”reader”></param>
        /// <returns></returns>
        public RuleSet Deserialize(Type factType, XmlReader reader)
        {
            RuleSet result = new RuleSet();

            ParserProxy parser = new ParserProxy(factType);

            //string allXml = reader.ReadOuterXml();
            XmlDocument xDoc = new XmlDocument();
            xDoc.Load(reader);
            result.Name = xDoc.SelectSingleNode(“//RuleSet.Text/@Name”).InnerText;
            result.Description = xDoc.SelectSingleNode(“//RuleSet.Text/@Description”).InnerText;
            result.ChainingBehavior = GetEnumValue<RuleChainingBehavior>(xDoc.SelectSingleNode(“//RuleSet.Text/@ChainingBehavior”).InnerText);

            foreach (XmlNode ruleNode in xDoc.SelectNodes(“//Rule”))
            {
                Rule rule = new Rule();
                rule.Active = ruleNode.SelectSingleNode(“@Active”).InnerText == “true”;
                rule.Description = ruleNode.SelectSingleNode(“@Description”).InnerText;
                rule.Name = ruleNode.SelectSingleNode(“@Name”).InnerText;
                rule.Priority = int.Parse(ruleNode.SelectSingleNode(“@Priority”).InnerText);
                rule.ReevaluationBehavior = GetEnumValue<RuleReevaluationBehavior>(ruleNode.SelectSingleNode(“@ReevaluationBehavior”).InnerText);

                // I have problem identifying rule condition…
                rule.Condition = parser.ParseCondition(ruleNode.SelectSingleNode(“Condition”).InnerText);

                foreach (XmlNode thenNode in ruleNode.SelectNodes(“ThenActions/Action”))
                {
                    List<RuleAction> actions = parser.ParseStatementList(thenNode.InnerText);
                    foreach (RuleAction action in actions)
                    {
                        rule.ThenActions.Add(action);
                    }
                }

                foreach (XmlNode elseNode in ruleNode.SelectNodes(“ThenActions”))
                {
                    List<RuleAction> actions = parser.ParseStatementList(elseNode.InnerText);
                    foreach (RuleAction action in actions)
                    {
                        rule.ElseActions.Add(action);
                    }
                }

                result.Rules.Add(rule);
            }

            return result;
        }
    }
}

=== Program ===

using System;
using System.Workflow.Activities.Rules;
using System.Workflow.Activities.Rules.Design;
using System.Xml;

namespace PerfectStorm.Rules
{
    public class Order
    {
        private string _comment;
        public string Comment { get { return _comment; } set { _comment = value; } }
        public void setComment(string s) {this.Comment = s;}
    }
   
    class Program
    {
        // My key point here is that the workflow serializer creates illegible rules.
        // I want to write something that stores the rules in something closer to the native form.
        static void Main(string[] args)
        {

            // Create a RuleSet that waorks with Orders (just another .net Object)

            RuleSetDialog ruleSetDialog = new RuleSetDialog(typeof(Order), null, null);

            // Show the RuleSet Editor
            ruleSetDialog.ShowDialog();
            // Get the RuleSet after editing

            RuleSet ruleSet = ruleSetDialog.RuleSet;

            Order o = new Order();
            RuleValidation rv = new RuleValidation(typeof(Order),null);
            ruleSet.Validate(rv);
            foreach (var err in rv.Errors)
            {
                Console.WriteLine(err.ErrorText);
            }
            ruleSet.Name = “Test”;

           // Console.WriteLine(Pickler.Pickle<RuleSet>(ruleSet));

           

            RuleEngine re = new RuleEngine(ruleSet, typeof(Order));
            re.Execute(o);
            Console.WriteLine(o.Comment);
 
            TextRuleSetSerializer serializer = new TextRuleSetSerializer();

            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            XmlWriter rulesWriter = XmlWriter.Create(“test.rules”, settings);
           

            serializer.Serialize(rulesWriter, ruleSet);
            rulesWriter.Close();

            // Deserialize from a .rules file.

            XmlTextReader rulesReader = new XmlTextReader(“test.rules”);

            TextRuleSetSerializer serializer2 = new TextRuleSetSerializer();

            ruleSet = (RuleSet)serializer2.Deserialize(typeof(Order), rulesReader);
            o.Comment = “”;
            re.Execute(o);
            Console.WriteLine(o.Comment);
        }
    }
}

Read full story Comments { 1 }

Encapsulation is an illusion in .NET

This is an article on how to call internal methods.

This allows any full trust code to call internal methods.  I can see that this is very dangerous and useful.
This should allow me to access the Parser class from the WF foundation assembly.

Read full story Comments Off

Simpler WF Rules Serializer

I have been looking at the Windows WF Rules serialization.  The default version creates 100 lines of illegible xml for each line of entered rules.  My version creates 10 lines per line of rule.  I am tempted to incorporate this into the stand-alone ruleset editor. My version would initially store the readable version alongside the compiled version.

Here is a version that creates cleaner XML:

    public class TextRuleSetSerializer
    {
        public void Serialize(XmlWriter writer, RuleSet rs)
        {
            writer.WriteStartDocument();
            writer.WriteStartElement(“RuleSet.Text”);
            writer.WriteAttributeString(“Name”, rs.Name);
            writer.WriteAttributeString(“Description”, rs.Description);
            writer.WriteAttributeString(“ChainingBehaviour”, rs.ChainingBehavior.ToString());
                       
            foreach (var rule in rs.Rules)
            {
                writer.WriteStartElement(“Rule”);
                writer.WriteAttributeString(“Active”, rule.Active.ToString());
                writer.WriteAttributeString(“Description”, rule.Description);
                writer.WriteAttributeString(“Name”, rule.Name);
                writer.WriteAttributeString(“Priority”, rule.Priority.ToString());
                writer.WriteAttributeString(“ReevaluationBehavior”, rule.ReevaluationBehavior.ToString());

                writer.WriteStartElement(“Condition”);
                writer.WriteAttributeString(“ConditionType”, rule.Condition.GetType().FullName);
                writer.WriteAttributeString(“ConditionAssembly”, rule.Condition.GetType().AssemblyQualifiedName);
                writer.WriteValue(rule.Condition.ToString());
                writer.WriteEndElement();

                writer.WriteStartElement(“ThenActions”);
               
                foreach(var action in rule.ThenActions)
                {
                    writer.WriteStartElement(“Action”);
                    writer.WriteAttributeString(“ActionType”, action.GetType().FullName);
                    writer.WriteAttributeString(“ActionAssembly”, action.GetType().AssemblyQualifiedName);
                    writer.WriteValue(action.ToString());
                    writer.WriteEndElement();
                }
               
                writer.WriteEndElement();
               
                writer.WriteStartElement(“ElseActions”);

                foreach (var action in rule.ElseActions)
                {
                    writer.WriteStartElement(“Action”);
                    writer.WriteAttributeString(“ActionType”, action.GetType().FullName);
                    writer.WriteAttributeString(“ActionAssembly”, action.GetType().AssemblyQualifiedName);
                    writer.WriteValue(action.ToString());
                    writer.WriteEndElement();
                }
               
                writer.WriteEndElement();
                writer.WriteEndElement();
            }
     
            writer.Flush();
        }
}

I am still working on the deserialization version.  This is more difficult since the code that is used in the RuleSetDialog to compile the entered text into CodeDOM is an internal class.  This leaves me with two options: 

  1. Use reflector to create a non-internal version of the same code.
  2. Puppeteer the RuleSetDialog to perform the compilation for me.
Read full story Comments Off

Visual Studio error: Unable to copy from obj\debug to bin\debug

Here is a discussion of an error that I also found.

One day I had lost the connection to the TFS server. This meant that my project could not access version control.  Once I synced up again and checked in my changes some of the dll’s in the bin\debug folder had been included in TFS.  This prevented me from building with the above error until these were excluded.

Read full story Comments Off

Useful C# 3.0 Language Feature

The project that I am currently working on has started to use C# 3 features so I thought that I would see what they were about.

Here is an explanation of automatic properties in C# 3.0.

Basically if you declare a property as:

public int FooBar {get; set;}

then the compiler will create the private storage for you.
If you do anything different then it will leave it to you.

Here is an explanation of extension methods.

Read full story Comments Off

Item Picker in SharePoint

This is an article on using an item picker in SharePoint

Read full story Comments Off

SharePoint css classes

Here is a set of documentation on the names of the standard css classes in SharePoint.

Read full story Comments Off

Adding BDC Columns to a Sharepoint List Programatically – working version

I have been working on a SharePoint project that currently requires over a hundred SharePoint lists to contain the data.  Each list consists of the columns from a Content type plus the data from a singe BDC entity.
To set that lot up by hand (which out of the box is the only way) would be far too time consuming. All of my lists exist at the root site level so that each of my various subsites may access them.

This code will create the list with all of the columns from the Content Type and all of the columns from the BDC Entity.  It then updates the default view of the list to show all of these.

This code does use one class that microsoft has marked as do not use in your own application.  But since it is the only way that I have found to get this to work….

Identifying the required using clauses is left as an exercise to the reader.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Xml;

using Microsoft.SharePoint;

using Microsoft.Office.Server;

using Microsoft.Office.Server.ApplicationRegistry.MetadataModel;

using Microsoft.Office.Server.ApplicationRegistry.Infrastructure;

using Microsoft.SharePoint.Portal;

using Microsoft.SharePoint.Portal.WebControls;

 

namespace AddSharePointBDCListNS

{

   

    /// <summary>

    /// This console application will add a SharePoint list to a site based upon a content type and a bdc entity.

    /// The content type will contain the key field for the bdc data so that the bdc data can be removed and replaced.

    /// </summary>

    class Program

    {

        /// <summary>

        ///

        /// </summary>

        /// <param name=”args”></param>

        static void Main(string[] args)

        {

           

            if (args.Length != 8)

            {

                Console.WriteLine(“AddSharePointBDCList http://site:1234 listName ssp contentType CoreInstance core.Trade_BigList_v TradeRef Trade”);

            }

            else

            {

                try

                {

                    AddSharePointBDCList(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);

                }

                catch (Exception ex)

                {

                    Console.WriteLine(ex);

                }

            }

           

        }

 

        static string ObtainBdcFields(string application,

                                      string entity,

                                      string entityTitle)

        {

            LobSystemInstance instance = ApplicationRegistry.GetLobSystemInstanceByName(application);           

            Entity requiredEntity = instance.GetEntities()[entity];

 

            List<string> rawField = new List<string>();

 

            foreach (Field field in requiredEntity.GetFinderView().Fields)

            {

                rawField.Add(field.Name);

            }

 

            rawField.Sort();

            StringBuilder fieldList = new StringBuilder();

            bool isFirst = true;

            foreach (string field in rawField)

            {

                if (!isFirst)

                {

                    fieldList.Append(“:”);

                }

                isFirst = false;

                fieldList.Append(field);

            }

            return fieldList.ToString();

        }

       

        /// <remarks>

        /// This is serious black belt sharepoint.  It required a lot of trial and error to get this far.

        /// I would like to be able to simply pass the application and the entity but have run out of time.

        /// </remarks>

        ///<summary>

        ///<param name=”siteUrl”></param>

        ///

        ///</summary>

        static void AddSharePointBDCList(string siteUrl,

                                         string listName,

                                         string ssp,

                                         string contentType,

                                         string bdcSystemInstance,

                                         string bdcEntity,

                                         string bdcField,

                                         string bdcDisplayField)

        {

            SPSite site;

            try

            {

                site = new SPSite(siteUrl);

            }

            catch (Exception ex)

            {

                throw new Exception(“Unable to find site (” + siteUrl + “)”, ex);

            }

           

            SPWeb web = site.OpenWeb();           

 

            SPContentType newCT;

            try

            {

                newCT = web.ContentTypes[contentType];

            }

            catch (Exception ex)

            {

                throw new Exception(“Unable to find content type (” + contentType + “)”, ex);

            }

 

            // I have moved these here since it will prevent a list being created if any of the data is wrong.

            try

            {

                SqlSessionProvider.Instance().SetSharedResourceProviderToUse(ssp);

            }

            catch(Exception ex)

            {

                throw new Exception(“Unable to find SSP (” + ssp + “)”, ex);

            }

 

            string rawFields;

 

            try

            {

                rawFields = ObtainBdcFields(bdcSystemInstance, bdcEntity, bdcDisplayField);

            }

            catch (Exception ex)

            {

                throw new Exception(“There is something wrong with at least one of the following:: bdcSystemInstance: ” + bdcSystemInstance + ” bdcEntity: ” + bdcEntity + ” bdcDisplayField: ” + bdcDisplayField , ex);

            }

 

            /* At this point we have all of the data that we need to recreate the lists! */

 

            // You can’t create a list based upon a user-defined content type but you can do this:

            // Add a generic list

            Guid listGuid = web.Lists.Add(listName, listName, SPListTemplateType.GenericList);

            // … Why that did’nt return the list I’ll never know, use the guid instead

            SPList list = web.Lists[listGuid];

 

            // Now mess with the content types:

 

                    SPContentType old = list.ContentTypes[0];

                    list.ContentTypes.Add(newCT);

                    list.ContentTypes.Delete(old.Id);

                    list.Update();

 

            // Now add the missing fields to the view.

            SPView view = list.DefaultView;

                    foreach( SPField field in newCT.Fields)

            {

                                if (!view.ViewFields.Exists(field.InternalName))

                {

                                            view.ViewFields.Add(field);

                }

            }

            view.Update();

 

 

         /* I think that this is now minimal */

         const string contentTemplate = @”<Field Type=’BusinessData’

         DisplayName=’{0}’

         Required=’FALSE’         

         StaticName=’{0}’

         BaseRenderingType=’Text’

         Name=’{0}’

         RowOrdinal=’0′

         SystemInstance=’{1}’

         Entity=’{2}’

         BdcField=’{3}’

         Profile=’/_layouts/ProfileRedirect.aspx?Application={1}&amp;Entity={2}&amp;ItemId=’

         HasActions=’True’

         RelatedField=’{4}’         

         RelatedFieldWssStaticName=’{4}‘         

         AddFieldOption=’AddToDefaultContentType, AddFieldToDefaultView’

         />”;

 

 

            string fieldContent = string.Format(contentTemplate,

                                  bdcDisplayField,                           //0

                                  bdcSystemInstance,                         //1

                                  bdcEntity,                                 //2

                                  bdcField,                                  //3

                                  bdcEntity.Replace(“.”,”_x002e_“) + “_ID”  //4

                                  );

           

            string fieldName = list.Fields.AddFieldAsXml(fieldContent);

            list.Update();

 

            // … You would think that this would be enough, you now need to convince the bdc column to save the

            // bdc specific details.

 

            BusinessDataField busField = list.Fields[fieldName] as BusinessDataField;

            busField.SystemInstanceName = bdcSystemInstance;

            busField.EntityName = bdcEntity;

            busField.BdcFieldName = bdcField;

 

            string[] names = rawFields.Split(new char[1] { ‘:’ });

            busField.SetSecondaryFieldsNames(names);

           

            busField.Profile = string.Format(“/_layouts/ProfileRedirect.aspx?Application={0}&amp;Entity={1}&amp;ItemId=”, bdcSystemInstance, bdcEntity);

            busField.HasActions=true;

 

            busField.Update();           

            list.Update();

                       

            // Lets see if the bdc fields can now be added…

            foreach (SPField field in list.Fields)

            {

                if (!view.ViewFields.Exists(field.InternalName))

                {

                    if (!field.Hidden)

                    {

                        view.ViewFields.Add(field);

                    }

                }

            }

           

            view.Update();

             

        }

    }

}

Read full story Comments Off