Stumbling Through

Join me as I stumble, bumble and fumble my way through some new developer technologies. We'll laugh, we'll cry, there may be a mouse tossed through a monitor, but in the end we will all hopefully learn something.
in

November 2007 - Posts

Stumbling Through: K2 Blackpearl (Automatic Workflows Part II)

We left off previously with a workflow that can automatically assign itself to an appropriate user after a set amount of time passes, effectively simulating a reoccurring workflow.  There was one major weakness, however, and that is that the time used to determine when to escalate the activity is based on the number of days/hours/minutes since the activity was last completed.  While this may be due to the design of the workflow, it lends itself to a good topic for a blog, and that is manually manipulating the code generated by blackpearl workflow items.  What I want to do is modify the escalation rule that we defined previously to execute every five minutes to instead escalate at the top of the hour every hour, regardless of when the last process was closed by the user.  Using our current rules, if the user closed the activity at 11:05am, it would escalate again at 12:05pm (if we made it escalate every hour).  What I want it to do, is escalate at 12:00 no matter when during the 11 hour it was closed.  Lets take a closer look at how we will accomplish this by opening the workflow we created in the previous post and opening its 'Activity Escalations' dialog:

image

In this dialog, we get the option to select an escalation and view its rule code:

image

Select this option, and we are presented with the Windows Workflow representation of this escalation rule:

image

We can double click the one code item in this workflow to bring us to its source code:

image

Right away, we can see the logic that sets the K2 Escalation rule equal to whatever values were entered into the escalation configuration dialog (The K2.Configuration items).  Instead of pulling the values from the configuration, we'll calculate the values based on our business rule, that is, since it should always escalate at the top of the hour, we will set 'EscalateMinutes' equal to however many minutes are left in the current hour:

 

private void EscalateAfterRule_ExecuteCode(object sender, EventArgs e)
{
    int EscalateDays = 0;
    int EscalateHours = 0;
    int EscalateMinutes = 60 - DateTime.Now.Minute;
    int EscalateSeconds = 0;
    int EscalateRepeatedTimes = K2.Configuration.EscalateRepeatedTimes;

    K2.SetEscalationRule(EscalateDays, EscalateHours, EscalateMinutes, EscalateSeconds, EscalateRepeatedTimes);
}

 

Save the changes and deploy the workflow, start it up and make sure it appears in the service account's task list:

image

Now we'll have to wait until the top of the hour to see if it automatically escalates to my task list.

*** Time Passes ***

image

Bam!  There it is in my task list, though oddly enough, the 'Event Start Date' is still the same as the original event start date.  I assumed it would update to the time at the top of the hour when it was redirected, but that is a minor issue considering it actually arrived at the top of the hour.

Posted: Nov 30 2007, 03:39 PM by tbyrne | with no comments
Filed under:
Stumbling Through: K2 Blackpearl (Automatic Workflows Part I)

One of the workflow tasks presented to me that required a proof of concept was to enable a workflow process that automatically assigns itself to the appropriate person annually, monthly, weekly or daily.  As usual, I stumbled through a solution without actually doing any research on whether or not there is a 'correct' or 'recommended' way to do this, so please feel free to critique what I've done here or use it as a K2 blackpearl learning exercise in how not to do things.  First things first, we need to create our process.  Right click your workflow project and add a new process, I've aptly named mine 'AutomaticWorkflow':

image

What we want to do with our nice blank-slate workflow is define a client event that will put an appropriately named task onto a service account's task list which will escalate to the appropriate owner's task list at the correct time (after a year, month, week, whatever).  The reason we assign it to the service account is simply so that the workflow task is alive and kicking, waiting for the appropriate time to become 'active' by assigning itself to a real user.  We have to trust that the service account user will never go in and close, reassign or otherwise tamper with this activity while it is hanging around on their task list waiting to escalate.  This workflow also needs logic to re-assign the workflow back to the service account when the 'real' user finishes it, resetting its internal escalation clock so it will escalate to the real user again at the appropriate time.  Let's get started with this workflow by dragging out a 'Default Client Event' activity wizard from the toolbar to our blank canvas:

image

Doing so will begin the default client event wizard which will ask us most of the important questions about our little workflow.  Click 'Next' to start the wizard, and we are presented with the 'Event Name and Forms' dialog.  We don't really care what this event is doing or what it is named, so just make it a web page action that opens up, oh, I dunno, my company's homepage:

image

Click 'Next' and we can ignore the next dialog which I believe will e-mail the recipient of the activity if you check the box.  Click 'Next' again and we come to the 'Configure Actions' dialog.  This is where we specify the various things that the user can do to this workflow, which for our purposes we will only allow them to close it.  This wizard has a handy feature that automatically adds the 'Complete Task' action if we don't specify any actions, so you can go ahead and click 'Next' without specifying anything, and answer 'yes' to the dialog that asks if it should create the default action:

image

image

We are then presented with the 'Configure Outcomes' screen, which details the various states the activity can be in.  Since we chose to automatically create the default action, it ties a default 'Task Completed' outcome in to that action so we can go ahead and click 'Next' without fiddling with anything:

image

Now the wizard is asking for the 'Destination User' of this activity, which is the account to assign the activity to once it starts.  We want to specify our blackpearl service account here, so that the activity is placed in its task list by default:

image

Click 'Next' and then 'Finish' to complete the wizard and your canvas should now look like this:

image

Connect the 'Start' box with our activity (rename the activity to 'Automatic Activity' so we can identify it when it appears on the task list) and connect the 'Task Completed' arrow back to the activity, like this:

image

What this means is that whenever a user completes this activity, it will revert back to its default state of being assigned to blackpearl service account with the escalation counter reset.  Escalation counter, you say?  What's up with that?  Well, I was just about to get into that... an activity can have 'escalations' assigned to it, which mean that on or after a certain amount of time, the activity can perform a number of actions including being moved from the current destination user to a new destination user, effectively moving it from the current task list to another user's task list.  To accomplish this, we need to run the 'Activity Escalations' dialog, which is available from the activity's 'activity strip', accessible by clicking the little dropdown arrow next to the activity's name:

image

In the activity strip, select the 'Activity Escalations' icon, which resembles a little clock.  This brings up the following Escalations dialog:

image

One activity can have multiple escalations, but in our case, we only need one:  Activate Activity.  Click the 'Add' button and name the new escalation:

image

Click 'Next' and we are provided with a list of possible escalation actions.  The action we want in this case is 'Redirect', which will redirect the activity to a different user:

image

Click 'Next' and we are presented with the 'Rule Details' dialog.  This is where we specify when the activity should be escalated, and while we have a number of options here, for the purposes of this blog (where we demand results fast), we will make the activity escalate every 5 minutes:

image

Something to note here is that the time specified here is based on the tie time that the process started - so the way we have things set up, the process starts whenever the user closes the current activity.  This can lead to some inaccuracies when trying to make annual/monthly/weekly repeating workflows, because if you set up the time to be every 7 days (weekly) expecting it to appear every Monday, if the user closes it once on a Tuesday then the next escalation of the workflow will be seven days from that Tuesday, which would be the following Tuesday.  I didn't see any options to do things like we see in Outlook scheduler, like 'every week on Monday' or 'January 1st every year' which would be very useful (at least in the approach I am taking to the problem).  Click 'Next' and it is now asking us for the 'Redirect Action', which is basically asking who do we assign this activity to when it escalates.  In our case, this will be the actual user or group that needs to address the task, I've assigned it to myself because it will help prove that the escalation is working:

image

Click 'Finish' and then 'Finish' again to complete the escalation definition.  We will now deploy the process to the server, and once that is complete, we'll need to use the Workspace to manually start up the process as we did not tie this process into any server events:

image

Immediately after starting the process, we should see it on the service account's task list:

image

Now, we will wait five minutes and we should see it removed from the service account's task list:

image

and added to the 'real' user's task list (note the user names at the top of the screen shots):

image

I will now complete the task:

image

which will remove it from my task list:

image

And put it back on the service account's task list:

image

We can wait another five minutes to prove that it will escalate to me again. 

There you have it, a workflow that automatically assigns itself to a specific user at a somewhat specific time.  I'd like to see more options on the escalation time spans, but the way we defined this workflow comes fairly close to addressing the business need.  It looks as if there is a way to manually manipulate the code generated by the escalation, which is likely what I'd need to do to add more complex time spans to the escalation logic.  We will take a look at that feature in my next post.

Posted: Nov 30 2007, 10:03 AM by tbyrne | with no comments
Filed under:
Stumbling Through: LINQ
Technorati Tags: ,,

At the very beginning of my blogging days, I mentioned 'LINQ' as a technology that I was very interested in Stumbling Through... then I never mentioned it again.  Well, never fear all you 'Stumbling Through' fans (yes, both of you), I haven't forgotten this interesting bit of technology and have decided to work it into our People - Addresses project that we started up for Stumbling Through WPF (take a look at my post history for more information on this project).  Here is the plan: 

We currently have three collections defined -  People, Addresses and States.  Addresses are children of People, while States are kind of a lookup table.  What we'll do is load the records for these collections from xml files using LINQ, with the interesting caveat of including the full state name as a read-only property in the address object even though it resides in the States collection (the address object currently has only the state abbreviation).  Let's get started by creating two xml files:  People.xml and States.xml.  Here is the data for each:

 

People.xml

<?xml version="1.0" encoding="utf-8" ?>
<People>
  <Person FirstName="Test" LastName="Guy">
    <Address City="Chicago" State="IL" />
    <Address City="Mundelein" State="FL" />
    <Address City="Bartlett" State="CA" />
  </Person>
  <Person FirstName="Another" LastName="One">
    <Address City="Chicago" State="IL" />
    <Address City="Mundelein" State="FL" />
    <Address City="Lviv" State="CA" />
  </Person>
  <Person FirstName="Someone" LastName="Else">
    <Address City="Huntley" State="IL" />
    <Address City="Addison" State="IL" />
    <Address City="Bartlett" State="CA" />
  </Person>
</People>

 

 

States.xml

<?xml version="1.0" encoding="utf-8" ?>
<States>
  <State Name="Illinois" Abbreviated="IL"/>
  <State Name="Florida" Abbreviated="FL"/>
  <State Name="California" Abbreviated="CA"/>
</States>

 

 

We'll include these in our 'Stumbling Through' project under a 'Data' folder.  Make sure that each file has the 'Copy if New'flag set and Build Action is 'None', so it will be copied into a data folder relative to the executable (it'll make it easier to find the data at run-time). Now I want to modify the 'Load' methods of each collection to pull their data from these XML files instead of inserting hard-coded items.  We'll start with 'StateCollection.LoadAll', as it will probably be the simplest.  Here is the full code for the StateCollection class, I'll explain the 'LoadAll' method afterwards:

 

using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;

namespace StumblingThroughWPFPartI
{
    public class StateCollection : ObservableCollection<State>
    {
        public void LoadAll()
        {
            this.Clear();

            XElement xmlElement = XElement.Load("Data/States.xml");

            foreach (State state in
                from xmlState in xmlElement.Descendants("State")
                select new State(xmlState.Attribute("Abbreviated").Value, xmlState.Attribute("Name").Value)
            )
            {
                this.Add(state);
            }
        }
    }
}

 

Wow, that is some crazy syntax in the 'LoadAll' method... what does it mean?  Firstly, we clear any items in the the list.  Then, we load up an XElement (Xml.Linq namespace) with the data in our external xml file.  We then iterate through each state identified in the xml file, instantiate a state object using the element's attribute values in the constructor, and add the state instance to our collection.

Something else interesting with the LINQ syntax is that if we wanted to populate our State object properties after instantiating it (that is, there was an empty constructor), we could substitute the following for our previous 'foreach' statement:

 

foreach (State state in
    from xmlState in xmlElement.Descendants("State")
    select new State()
    {
        Abbreviated = xmlState.Attribute("Abbreviated").Value,
        Name = xmlState.Attribute("Name").Value
    }
)

 

This instantiates the State object, and then sets its 'Abbreviation' and 'Name' properties all in the LINQ statement!

I am very impressed with LINQ so far, even if the syntax is a little confusing (why 'from' element 'in' source?  Just to avoid confusion with 'for'?).  Let's move on to our 'PersonCollection.LoadAll', which should be just as simple:

 

using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;

namespace StumblingThroughWPFPartI
{
    public class PersonCollection : ObservableCollection<Person>
    {

        public void LoadAll()
        {
            this.Clear();

            XElement xmlElement = XElement.Load("Data/People.xml");

            foreach (Person person in
                from xmlPerson in xmlElement.Descendants("Person")
                select new Person(xmlPerson.Attribute("FirstName").Value, xmlPerson.Attribute("LastName").Value)
            )
            {
                this.Add(person);
            }
        }
    }
}

 

 

Nothing new here, just using the 'Person' object and xml data.  Let's see how 'AddressCollection.LoadByPerson' will work out:

 

using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Generic;

namespace StumblingThroughWPFPartI
{
    public class AddressCollection : ObservableCollection<Address>
    {
        public void LoadByPerson(Person parent)
        {
            this.Clear();

            XElement xmlElement = XElement.Load("Data/People.xml");
            IEnumerable<XElement> xmlParentElement = from item in xmlElement.Descendants("Person")
                         where item.Attribute("FirstName").Value.Equals(parent.FirstName)
                         where item.Attribute("LastName").Value.Equals(parent.LastName)
                         select item;
            xmlElement = xmlParentElement.First();

            foreach (Address address in
                from xmlAddress in xmlElement.Descendants("Address")
                select new Address(xmlAddress.Attribute("City").Value, xmlAddress.Attribute("State").Value)
            )
            {
                this.Add(address);
            }
        }
    }
}

 

 

All right, things got a little hairy here.  After clearing the list, we need to first find the parent element in the xml data because we are loading all addresses for a given person.  So I load all people, then set an enumerable list (the default return type for a LINQ query) equal to the person element that has both a first and last name equal to the first and last name of the parent person (obviously, we should use a better key here but we'll take what we got for now).  We then set our element equal to the first item in said enumerable list, which should have one and only one item in it if we found our parent correctly.  Once we get the parent, then the rest of the LINQ stuff looks like what we did for the other load methods.

So far, everything has gone pretty smoothly, but we really haven't tried to do anything complicated that utilizes one of the features of LINQ that I find most intriguing.  Let's remedy that by combining our StateCollection object with the xml query used to load an address collection.  I want to grab the state name based on the state abbreviation, and stick it into the address as a read-only property.  We'll first need to modify our Address class' fields, constructor and properties to contain a 'StateName' value (read only property).  Here is the code for the new Address class:

 

using System;
using System.ComponentModel;

namespace StumblingThroughWPFPartI
{
    public class Address : INotifyPropertyChanged, IDataErrorInfo
    {
        String _city;
        String _state;
        String _stateName;

        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
        {
            PropertyChanged(this, e);
        }

        public Address(String city, String state, String stateName)
        {
            _city = city;
            _state = state;
            _stateName = stateName;
        }

        public String City
        {
            get
            {
                return _city;
            }
            set
            {
                _city = value;
            }
        }

        public String State
        {
            get
            {
                return _state;
            }
            set
            {
                _state = value;
            }
        }    
        public String StateName
        {
            get
            {
                return _stateName;
            }
        }

        public string Error
        {
            get
            {
                return null;
            }
        }

        public string this[string name]
        {
            get
            {
                string result = null;

                if (name.Equals("City"))
                {
                    if (this._city.Trim().Length == 0)
                        result = "Please specify a city";
                }
                else if (name.Equals("State"))
                {
                    if (this._state.Trim().Length == 0)
                        result = "Please specify a state";
                }

                return result;
            }
        }

    }
}

 

 

With that out of the way, our code will now no longer compile because the 'AddressCollection.LoadByPerson' LINQ statement that instantiates the Address object now requires the 'StateName' parameter in its constructor.  How do we get it?  Well, here is the code, which I will explain afterwards:

 

 

public void LoadByPerson(Person parent)
{
    this.Clear();

    StateCollection allStates = new StateCollection();
    allStates.LoadAll();

    XElement xmlElement = XElement.Load("Data/People.xml");
    IEnumerable<XElement> xmlParentElement = from item in xmlElement.Descendants("Person")
                 where item.Attribute("FirstName").Value.Equals(parent.FirstName)
                 where item.Attribute("LastName").Value.Equals(parent.LastName)
                 select item;
    xmlElement = xmlParentElement.First();

    foreach (Address address in
        from xmlAddress in xmlElement.Descendants("Address")
        join state in allStates on address.Attribute("State").Value equals state.Abbreviation
        select new Address(xmlAddress.Attribute("City").Value, xmlAddress.Attribute("State").Value, state.Name)
    )
    {
        this.Add(address);
    }
}

 

What we do now after clearing the list, is load all of our states into a collection utilizing the methods we already have in place.  We then get the parent 'Person' element same as before.  Now, when we are looping through each address to instantiate an object for it, we join to our States collection by matching the 'State' attribute in the xml of the address to the 'Abbreviated' property of the state objects.  After doing so, the 'state' LINQ element will be the state with the addresses abbreviation, and we can use its Name to pass along to our address constructor.  To prove whether this is working, lets modify our front-end bindings so that the list box that used to be displaying the address' 'City' is now displaying its 'StateName'.  Run the application and we get... an error?  What the heck?  This worked fine in a different project, but now I am getting:

Argument '3': cannot convert from 'lambda expression' to 'System.Func<System.Xml.Linq.XElement,string>'

Well now, let's try and figure out what is going wrong here.  It is clearly an issue with our join, but what?  Oh, duh, my bad... it should join on xmlAddress.Attribute("State"), not address.Attribute("State")... address is our object instance, it doesn't have an 'Attribute' property.  Could have given me better feedback than that!  Change our foreach to this:

 

 

foreach (Address address in
    from xmlAddress in xmlElement.Descendants("Address")
    join state in allStates
    on xmlAddress.Attribute("State").Value equals state.Abbreviation
    select new Address(xmlAddress.Attribute("City").Value, xmlAddress.Attribute("State").Value, state.Name)
)

 

Run the application and we get this:

 

image

 

Now THAT'S what I'm talking about!  This sort of functionality is nothing new to object-oriented programming, but never before has it been so integrated into the language and provided such short and intellisense-enabled coding.  Including custom collections in a query on XML (that could have just as easily been a database, by the way) is very powerful and can help streamline many collection-oriented architectures with various data sources which probably makes up a good chunk of the architectures out there.  I'm sure LINQ can be useful in any other situation as well, maybe not as a replacement for SQL (though definitely can replace XPATH for THIS developer), but at least as a common query language/routine for inter-object queries and interfaces to business objects.  I hope I impressed some of the basics of LINQ to you while giving you a real-world (albeit simple) example of its use.  If not, well, I hope my stumbling through it at least provided you several moments of entertainment even if I didn't chuck a mouse through my monitor (yet).

Stumbling Through - K2 Blackpearl (Smart Objects - Part IV)

So where are we?  We have a fully defined SmartObject deployed on our SmartObject server, we have an InfoPath form and we have a workflow process that will tie it all together... the one thing that is missing here is the glue.  As with most things I've been stumbling into, the 'glue' turned out to be surprisingly easy to locate once I sat down and thought about it.  The only place that can tie an InfoPath form with data, would be the InfoPath form itself.  When I first created my form, though, I didn't see any way to bind it to a smart object.  However, after going through the InfoPath integration wizard and having my form become a part of the project, that may have changed.  Let's open up the InfoPath form from the Visual Studio project itself.  This brings it up in run-mode, but we want to be in design mode so click 'Tools -> Design This Form'.  When we visit the 'Data Source' section, notice that it is now populated with several datasources:  'Main' seems to contain the properties of our Smart Object.  Our 'Create' methods appear as individual data sources.  Two additional data sources are available as well, though their purpose isn't clear to me as of yet:

image

So I guess it was foolish of us to design our form before integrating it into K2, because now we can simply drag and drop the columns from our data source:

image

Now I dragged all of these fields out from the 'Main' data source, though I bet I'd have to use the ones from one of the 'Create' methods in order for it to work as we want.  However, we'll proceed with this and see what happens.  Save the design, then re-open the form from Visual Studio and submit it after entering in a few values.

When we go out to SharePoint and view our K2 tasks web part, we can see that it must have gotten at least to the part in our workflow that said to create the task:

image

However, visiting the 'Clarity Assets' list we see that it is still empty... our 'Create' method did not execute.  Since it hit our 'create K2 task' event, we can safely assume that it is a problem with our InfoPath form, which is likely because at no point did we tell it to invoke one of our 'Create' Smart Object methods.  While that may ultimately be true, I want to play around with another feature first to see if it will work too.  Let's add a 'SmartObject Event', making it the first thing to be invoked when the workflow starts.  We'll make this event call our 'CreateLaptop' method.  This is where we get a little constrained, as we can't tell it to call a different method depending on the type that was selected in the form.  Oh well, I just want to see if it'll work!  Here is what our workflow should look like now:

image

Deploy and then submit another request and... crap, that didn't create the list item either!  Let's remove that smart object event that we just added and get back to our InfoPath form, there has got to be some way to tell it to run our method on submit!  Not being an InfoPath expert, it wasn't very obvious to me to find the 'Tools - Submit Options' menu, but once I was in there it was pretty straight forward to add a rule that queries our 'CreateLaptop' method before it creates the workflow.  Now I believe I will also remove the fields we have in there and instead use the 'CreateLaptop' fields:

image

We can probably clean these up, I just want to see something working!  Save this new design and re-open the form.  Enter in some data and submit it:

image

Hold your breath, and check the Clarity Assets list...

image

Yes!  There it is!  I am going to go get myself a coke and give this new information some time to digest.  I will probably completely re-do the project now that I kind of know how things work, and will post a more 'correct' version for your reading pleasure.

Stumbling Through: K2 Blackpearl (Smart Objects - Part III)

We left off previously with a well-defined smart object containing all of our asset properties and methods to create each type of asset.  Now, we need to make a place to consume this smart object.  I am choosing to use InfoPath, though Blackpearl can integrate with ASP .NET just as well.  Time permitting, I will do this project in both, but the InfoPath -er- path seems easiest for our goals right now.  So what we want to do is create an InfoPath form that first prompts the user to select an asset type, and then, depending on the type selected, fill in values for the columns of the selected type.  When done, the user will click 'Ok' and the asset will be created with a status of 'Requested', and it will be sent to an 'Asset Administrator' for approval.  Now this isn't a post on 'Stumbling through InfoPath', so I'm not going to go into much detail on the InfoPath form... I made it as quick and dirty as possible just to get it out there.  I didn't create any logic, I simply created the input fields and plopped an 'Ok' button on it.  I want to see just how much the Blackpearl integration with InfoPath with do for us.  So let's assume we've created this form and saved it on our local (development) machine.  We'll go back into our Visual Studio project, and add a new workflow called 'RequestAnAsset':

image

We need to tell this workflow that it will be integrating with InfoPath, so the first thing we need to do is drag out an 'InfoPath Integration' wizard:

image

I will now walk through each step of the integration process:

image

Click 'Next' to start the process.  In the first step, simply add a reference to the InfoPath form that is stored on your local drive:

image

It will then ask where to publish the form when the project is deployed.  I didn't make a space on SharePoint for these forms, but thankfully the wizard gives us the option to create a new space:

image

image

image

Click 'Next' and we are prompted to specify which Smart Object methods will be integrated with the form.  Well, we want to form to be able to create any one of our asset types, so lets add all of our 'Create' methods.  I am browsing through the context browser for our smart object methods like this:

image

But it is not allowing me to drag the method to the 'Method' entry field... I wonder if the Smart Object needs to be deployed before I can integrate with it?  I went back to the Smart object definition and deployed it, accepting the defaults in the deployment wizard:

image

Let's try again to integrate with InfoPath.  This time, though, make sure to browse for the forms library we created instead of creating another one:

image

When we are integrating the methods, we should now be able to drill into our smart object in the 'SmartObject server' section instead of the 'Local' section.  At first, I didn't see anything different but as we learned previously, we should refresh the context browser before throwing the mouse through the monitor:

image

Add and drag each of our Create methods:

image

Click 'Next' and the wizard now asks us if we want to us a custom task pane for the form template.  Hell if I know!  I'll select the K2 Blackpearl task pane and make a note to ask somebody what exactly this means, though it may become obvious as we progress through the deployment:

image

By the way, if you do not see that task pane as an option, it may not have been deployed to your SharePoint site.  This was a confusing part of the Blackpearl deployment and integration process, so I'll give you a hint:  Log in to your SharePoint server's central administration site and click the 'Operations' tab.  Under 'Global Configuration', select 'solution management'.  Click the k2worklistwebpart.wsp file and deploy it in the resulting menu.

Let's get back to our workflow process, where we want to click 'Finish' to close out the smart object binding wizard.  We are now back to our InfoPath Integration wizard with the smart objects bound to our InfoPath form.  Let's click 'Next' and see what else it wants from us:

image

It is asking us which view of which form starts the workflow.  Since we only have one form and one view, this is an easy one to answer.  It is also asking us to specify a 'Process Folio'.  I am not clear on what this means, but it isn't required so let's skip it.  We should be able to accept the defaults for the 'Advanced Settings' screen, and the next step is the final step:

image

Would we like to configure a client event now?  Sure!  Check the box and click 'Finish' to start the InfoPath client event wizard:

image

Click 'Next' and it begins by asking us to name our event and specify the InfoPath view and form to use.  We can name it whatever we want, and the view and form options are no-brainers (there is only one form with one view to choose from).  'Task Action Field' is a little ambiguous, let's leave it blank for now and see what happens.  Click 'Next' and it asks us if we want to send a notification of the event to the end user.  Check the box, it will be helpful to know that our event is working correctly.  Click 'Next' and we are prompted to configure actions.  I guess this is where we specify that whomever gets notified of this request needs to Approve or Decline the request, so we'll make those our actions:

image

Click next, and it asks us to configure outcomes.  There are only two outcomes to this event, either the request is approved or it is declined.  Since we had the checkbox checked on the previous page, it automatically created those for us:

image

That's convenient, click 'Next' and it prompts us for the 'Destination Users'.  These are the users that will be notified of the pending request approval.  We should make a group for those users, but for now, just put yourself in there:

image

Click 'Next' and then 'Finish' to create the client event.

So here is what it gives us:

image

Note that it included our InfoPath form with the project.  The form is now being managed by K2, which is pretty useful as we'll get version control and K2 integration automatically.

Now what was completely missing here is how the heck do we tie the InfoPath fields to the SmartObject values?  Or, I guess the more accurate question would be how do we invoke our 'Create' methods from InfoPath with the values the user entered into the InfoPath fields?  I was hoping the wizard would encompass some of that.  Before we answer that question, lets tie some events to the 'Approve' and 'Decline' outcomes of the client event and deploy our workflow.  I'll just have it send me an e-mail in either case, just to have something valid out there.  The process for doing so is straight forward using the 'Mail Event', so I won't cover it in detail:

image

Deploy this and select the default deployment options.  Whew!  All right, well, we covered a lot of material here but didn't get any closer to our goal of integrating with InfoPath; at least not on the surface.  We need to figure out how InfoPath interfaces with our SmartObject methods, and how the values the user specifies can become property values of said SmartObject methods.  Stay tuned, and we'll stumble our way through that (I hope).

Stumbling Through: K2 Blackpearl (Smart Objects - Part II)

We left off last time hitting a wall in our attempt to create our custom 'Create' methods for each of our asset types.  For some reason, we weren't getting the columns we expected when we linked the Smart Object to our Clarity Assets list.  Even after refreshing our smart object service, we still didn't see the columns we were expecting.  Well, what I failed to do while we were stumbling around in there was to actually refresh my local view of the Smart Object Server after refreshing the service definition.  Duh!  The 'refresh' command is available off of the little dropdown menu next to 'Environment' in the Context Browser.

Anyway, lets get back to business and create our 'CreateLaptop' method:

image

image

Ahhh... That looks much better.  We can bind directly to our Laptop content type's 'Create' method.  NOW we can use 'Create All' to create local variable mappings of all the content type columns:

image

Repeat this process for Monitors and Hard Drives, so we have a total of three create methods:

image

Notice now that we also got a 'Content Type' property for free out of this... that will be useful when getting/setting the asset type of the request.  I'm beginning to wonder if we need to create those properties manually at all, or if they would've been created when we added the Create methods.  I'm gonna start from scratch again to prove that out.

Jackpot!  That is exactly what that 'Create All' button does when you are mapping your fields.  Live and learn:

image

Great, so, we have our nicely defined Smart Object, what do we do with it now?  That is a post for another day...

Stumbling Through: K2 Blackpearl (Smart Objects - Part I)

K2 is a company that specializes in Workflow software, the current version of which, 'Blackpearl', has become the target of my stumblings.  I was charged in using K2 Blackpearl in my 'Asset Tracker' project that I've discussed in previous posts.  Any workflow components of tracking assets need to be done using Blackpearl to give me some experience using the product.  The workflows involved in the Asset Tracker project are fairly simple, Blackpearl may be overkill for them as SharePoint 2007 itself provides some basic Workflow functionality that may better suite my needs but when I say 'basic', I MEAN 'basic'... Blackpearl is far more powerful than SharePoint workflow while still retaining the visual components that make it easy to use.  So what workflow aspect do I need to tackle for the Asset Tracker project?  First and foremost, I would like the act of requesting an asset to be a workflow process.  Simply put, a user will request an asset, which will then go to an approver who will either approve or deny the request.  For now, that is all that I want to do.

In K2 Blackpearl, there are two workflow designers available - there is one web designer integrated directly into SharePoint, and one integrated into Visual Studio 2005.  I will be using the designer in Visual Studio, so I can have the true developer experience of using the product as the web interface is targeted more at business users and may be somewhat less powerful.  I am assuming you have the Blackpearl product installed on your SharePoint 2007 server as well as the client tools installed on your Visual Studio machine.

Start up Visual Studio 2005, and create a new project.  If Blackpearl was installed correctly, you'll see project templates for 'K2'.  Select 'K2 Workflow Project' and name the project 'Clarity Asset Workflows':

image

Delete the default process that is created, so we have a nice empty environment:

image

Now we have to take a step back and think a bit about what we are trying to achieve.  I want the users to start the workflow by opening an 'Asset Request' form, where they will specify the type of asset (Laptop, Monitor, Hard Drive) and fill in the details of the request based on their selection.  When they click 'Save', I would like an asset of the appropriate type to be added into our Clarity Assets lists (created in a previous post) with an Asset Status of 'Requested'.  At this point, I would like the Approval Administrator (me, in this case) to be notified of the request and given the option to approve or decline the request, maybe even including some information on the request (a link to the item in the list, perhaps?).  If I approve the request, the status of the item in the list should be set to 'Approved', otherwise it will be set to 'Denied'.  I'd like to do all of this utillizing not only Blackpearl's workflow features, but also a concept new to Blackpearl called 'Smart Objects'.  Smart objects are a way of holding on to sets of data throughout the lifetime of the workflow that can be linked to underlying data sources so that changes made to the smart object are propagated to the data source.  One feature of smart objects is that they can be bound to SharePoint lists, which is exactly what we want in our scenario.

So let's begin by creating our 'AssetRequest' smart object.  Right click our 'Clarity Asset Workflows' project and select Add - New Item.  Select 'SmartObject' from the menu and name it 'AssetRequest':

image

Let's first flip to 'Advanced' mode and clear out all of the methods that were created by default, I like to start from a clean slate.  For the SmartObject properties, we are going to define every possible property for every possible asset type.  The reason for this is that we will be using the same smart object regardless of the asset type selected by the user.  So we will need to add 'Asset Status', 'Manufacturer', 'Model', 'Serial Number', 'Screen Size' and 'Capacity', all of them 'Text' properties:

image

We will now need 'Create' methods for each of our asset types.  Our form will invoke the appropriate 'Create' method depending on which Asset Type the user specifies.  Here is the progression of adding our 'CreateLaptop' method after clicking the 'Add Method' button:

image

For the 'Add Service Object Method' screen, utilize the 'Context Browser' (available via the ellipsis button) to locate the 'Create' method on our list, and drag it into the 'Select Service Object Method' field:

image

I'm not sure at this point why it only brought across 'Title' as an input property name... likely it is because all of the other columns are defined in content types.  We'll need to revisit this, as we will not be able to add a new list item with an Asset Status of  'Requested' if that attribute is not available for binding.  For now, though, let's just 'Create All' bindings to automatically create the equivalent of local variable instances for each of the values returned and required for input on the Create Clarity Asset method:

image

Click 'Next' and then 'Finish' to complete the creation of our method:

image

Something interesting that took place here is that it automatically created 'ID' and 'Link to Item' properties for us, which will be very useful when we want to pass along list-specific information to the various steps of our workflow.

Ah, I just had a thought about our problem retrieving all of the columns associated with our list.  Blackpearl retrieves these values from the Smart Object Server, which is supposed to be synchronized with SharePoint.  That synchronization does not take place automatically, we need to visit our SharePoint site and invoke the "Update K2 SmartObject Service Definition' Site action, like so:

image

Let's delete our 'CreateLaptop' method and try to re-create it (I'm sure we could edit it, but as usual, I like to start fresh).  Oops!  Hmmm... looks like my interpretation of the problem was incorrect.  While it probably isn't in my best interests to post my mistakes, I'll leave this in so that maybe others can learn from it.  I will go off now and find the resolution to this problem, because I believe that without a binding to the list columns, we won't be able to set their values when we call our 'Create' methods.  Maybe I am completely wrong in how this is supposed to work, but that is why this blog is called 'Stumbling Through' not 'Holding your hand through'.  I'll get it resolved, and continue my posting.

Stumbling Through: SharePoint 2007 (Content Types - Part III)

While in the previous 'Content Type' stumbling through posts we ended up with something very useable, I thought I'd try one last thing to hammer home the power of content types and their object-oriented nature.  In our current example, we allow the user to track three types of assets:  Laptops, Monitors and Hard Drives, each of which have their own custom attributes in addition to common attributes inherited from a base 'Assets' content type.  What we neglected to do in the first pass, however, is create a property that tracks the asset's current status, that is, whether it is owned, in transit, lost, destroyed, etc.  This is an attribute that all content types will need to have, and the value should come from a list of pre-defined asset statuses.

The first order of business here is to create our simple 'Asset Statuses' list.  We'll do so by clicking 'Site Actions' and then 'Create':

image

Then select 'Custom List' to create:

image

Name the new list 'Asset Statuses' and ensure it gets placed on the Quick Launch menu:

image

After clicking 'Create', we are taken to the list edit screen.  Simply add a few Asset Statuses to the list, like 'Owned', 'Ordered', 'Lost', etc.:

image

So now that our list is set up, we need to revisit our Assets content type to add the 'Request Status' column.  To get back to our Assets content type, click the 'Site Actions' and then 'Site Settings' option:

image

Click 'Site content types' to get back to our list of content types:

image

From the list of content types, click our 'Assets' content type which was created in the previous posts:

image

Here, we can add our new 'Asset Status' column via the 'Add from new site column' link:

image

Name the new column 'Asset Status', and put it into the 'Clarity Asset Columns' group.  For the type, though, select the 'Lookup' action.  When this option is selected, new information appears in the 'Additional Column Settings' section that asks where the lookup values should come from.  We want to specify 'Asset Statuses' for the 'Get Information From' section, and 'Title' is the column to display:

image

Clicking 'Ok' adds the 'Asset Status' column to the 'Assets' content type, which effectively adds this column to the Laptop, Monitor, and Hard Drive content types which inherit from Assets.  We can prove this out by visiting our 'Clarity Assets' list off of the home page and adding a new item.  In this case, I chose to add a new 'Hard Drive' content type and we can see that it now accepts a value for 'Asset Status' from a dropdown populated with all of the statues we defined in our 'Asset Status' list:

image

I love this approach to data management in SharePoint 2007.  Object oriented behavior has always held a place near and dear to my heart, mostly because it allows me to do things like we did in this post - make additions to the behavior of all my objects with very little coding.  As the business rules for this asset tracking project get fleshed out, I can just add to what I have already defined, without having to redefine anything or start from scratch.

I hope I've helped clarify content types, as well as providing a real-world example on how they can be used.  As usual, my stumbling through feels as if I've only scratched the surface of this technology, but as the project evolves I will be sure to blog any additional gotchas or features that I stumble across.

Stumbling Through: SharePoint 2007 (Content Types - Part II)

We left off last time with a definition of a 'base class' of sorts, our 'Asset' content type.  I call it a base class because it contains the properties (columns) that are common to all assets, that and we will never create an 'Asset', rather we will be creating Laptops, Monitors and Hard Drives.  Lets get started with that right now by creating our 'Hard Drive' content type.  As before, click 'Site Actions' and then 'Site Settings', then 'Site Content Types' from the menu that appears.  Click 'Create' and name the new content type 'Hard Drive'.  This time, when it asks us for the 'Parent content type', we can select our 'Clarity Assets Content Type' group to filter down to our 'Asset' content type to derive from.  This will automatically get us all the columns from the 'Asset' content type.  We will specify to add this content type to our 'Clarity Assets Content Type' group (yeah yeah, I spelled it wrong, so what?):

image

After we click ok, we can see the columns associated with the content type.  Title is there because that is Asset's parent content type, and the rest are there because we are deriving from Asset; all of this is easy to see in the 'Source' column.  Add the 'Capacity' column similarly to how we added all of the other columns:

image

Let's follow the same process to add 'Monitors' with their 'Screen Size' column and 'Laptops' without any special columns.  Your content types site gallery should look like this:

image

And your site columns gallery should look like this:

image

Now we need to create a list that will consume these content types.  Go to 'Site Actions' - 'Create':

image

Select 'Custom List' from the create menu:

image

Name the list 'Clarity Assets', and for ease of use, make it available from the quick launch menu:

image

Ok, great.  We have our list, but it doesn't seem to have anything to do with assets... how do we hook it in to our content types?  Good think I stumbled through this already so I can walk you through it.  On the Clarity Assets list, click 'Settings' and then 'List Settings':

image

In the 'Customize Clarity Assets' screen, select 'Advanced Settings':

image

Well well, looks like the very first option here deals with content types... 'Allow management of content types?' it asks?  YES!

image

After clicking ok, we are brought back to the 'Customize Clarity Assets' screen, only this time there is a section for 'Content Types':

image

By default, the list manages 'Items'.  This is all well and good, but we want it to manage our various asset content types.  Note, however, that we do NOT want to have it manage our base Asset content type... allowing it to do so would allow the user to add 'Assets', which wouldn't really mean anything in a business sense.  Use the 'Add from existing site content types' link to add our three content types:  Laptop, Monitor and Hard Drive.  Our 'Clarity Asset Content Types' grouping comes in real handy here, as selecting it limits our options to only our stuff:

image

Click ok and we can see that the list will now manage these asset types, which means that they will be available from the 'Add' menu and editing them will take advantage of the columns assigned to them.  We still have 'Item' associated with this list though, and it wouldn't make sense to allow the user to add an 'Item', so lets remove that.  Click it and say 'Delete this content type':

image

That sounds scary, like it is going to delete our base class, but it only removes its association from this list.  Now we are good to go, get back to the list itself (click 'Home' and then 'Clarity Assets').  Still looks the same to me, but now if you click the 'New' dropdown, you'll see the option to add only the content types that we defined:

image

Not only that, but if we actually select one of those items, say, 'New Hard Drive', we are presented with a data entry screen specific to the columns of a Hard Drive (per our content type definition):

image

Cancel that add, there is one other thing we need to do to make our list somewhat complete.  By default, it is showing only the 'Title' of all our assets.  No matter what type of asset it is, we know that it will have Title, Manufacturer, Model and Serial Number so why don't we always display those columns by default?  To do so, click the dropdown next to the 'All Items' view and select 'Modify this view':

image

Among other things, we can specify the columns to display in this view.  It retrieves all the possible columns based on the content types associated with this list, so we have the option to select 'Capacity' or 'Screen Size', but we shouldn't because not every item in the list will have values for those.  We would create special views for 'Hard Drives' and 'Monitors' to account for that.  For now, just select the columns we know every asset will have:

image

Now our list is looking a little more informative:

image

As I said earlier, lets create a custom view for each of our content types, including the columns that are specific to them.  You can do this by clicking the dropdown next to 'All Item's and selecting 'Create View':

image

Select 'All Items' to base this view on that one, it will give us all the default columns:

image

Name the view and add the columns you want, in this case I named the view 'All Hard Drives' and selected 'Capacity' in addition to the columns already selected.  Now we need to tell it to only show records with a content type of 'Hard Drive'.  This functionality can be achieved via the 'Filter' property.  Specify it as follows to achieve our desired results:

image

Click 'Ok' to save, and repeat the process for a 'All Monitors' and 'All Laptops' view.  When you are done, start adding some records of various types and use the new views to see how they work.

Stumbling Through: SharePoint 2007 (Content Types - Part I)

While I do get to play a lot with WPF in my spare time, I still have to do some real work from time to time.  That real work, for the time being anyway, involves using SharePoint 2007 and K2 Blackpearl to create a system for managing the assets of our company.  I am completely new to both of these products, so there is going to be a lot of stumbling and bumbling here; please bear with me.  I'm going to skip some of the business requirements stuff for the purposes of this blog and skip straight to the heart of the matter:  We need to track many different kinds of company assets, each asset having their own unique attributes in addition to a number of attributes common among all assets.  Sounds like something I read in a textbook in college to define the concept of object oriented programming... so how can I use that to my advantage given my technology choices?  Lucky for me, my wife is currently becoming a SharePoint 2007 expert herself while doing Q/A work for another company, and when I told her my dilemma, her advice to me was simply:  'Content Types'.  Very well then, content types it is, but what are they, exactly?  Rather than a long-winded and confusing attempt at describing what they are, I am just going to jump into an example that uses them to the best of my crude abilities.  I am going to assume that you have SharePoint 2007 installed, and that you have yourself a nice empty site to play with as an administrator.  Now there are a bazillion different ways to install and configure SharePoint, so your screens may not look identical but hopefully the functionality will end up being the same.

Our first order of business will be to define our content types as related to this site.  For the sake of this demo, lets say that I need to track Laptops, Monitors and Hard Drives as company assets.  Each of these items will have a manufacturer, model and serial number.  Monitors will additionally track 'Screen size', while hard drives will track 'Capacity'.  Laptops will track a number of different things, we want to leave that open for expansion.  To start defining content types, click your 'Site Actions' dropdown and select 'Site Settings':

image

Of the dazzling array of options we have at our disposal here, we want to select 'Site content types', which is the second option under the 'Galleries' section:

image

While your screenshot will likely differ than mine, what should be displayed is a list of content types already defined for the site by default:

image

Click the 'Create' button to bring up the 'New Site Content Type' screen.  I will walk through each of the options presented here:

image

The first attribute, Name, is how this content type will be referred to from here on out.  Description simply helps identify the purpose of the content type.  'Parent Content Type' is required, as all content types must derive from something.  I've found that the 'Item' content type is the most basic.  The first dropdown filters the items that appear in the second dropdown, making it easier to locate specific content types.  Finally, in the 'Group' section, we are specifying that we want this content type to be added to a brand new group.  This is the group that we will be adding all of our asset-related content types, and it will appear in the 'Parent Content Type' dropdown to help us find asset content types later.  Click 'Ok' to save this new content type, and we are brought to a page that lets us further define our content type:

image

What we are specifically interested in here is the 'Columns' section, in which we see 'Title' as the only defined column.  'Title' was inherited by our 'Item' parent content type.  Columns define the data that can be attributed to our content type, which if I were to go back to thinking 'object orientated-ly', means that the content type is our 'Class' and each column is a 'Property' of the class.  So we will use the 'Add from new site column' action to create and associate our base asset columns that we defined previously - namely:  Manufacturer, Model and Serial Number.  For simplicity sake, we will make them all free-form text columns, though later I will hopefully show you how to make them into dropdowns from pre-populated lists or other fancy options.  Click the 'Add form new site column' option, I will explain each item in the following screen:

image

The 'Name' is simply the name of the column as it will appear in any consuming content type.  The 'Type' gives us a number of options that will affect the behavior of the column when it is being edited.  There is a lot of power here, but we'll keep it simple and select 'Singe line of Text' for a free-form text field.  The next section, group, will group the column into 'Clarity Asset Columns', to make it easier to find later.  I have accepted the defaults for all of the other options.  Click 'Ok' to create the column and associate it with the Asset content type.  One thing to note here is that even though we created the column through the content type details screen, the column is not specific to that content type... it is created publicly and can be used in other content types.  One unusual caveat is that in order to make this column required, we have to go back in and edit it to set that property.  For some reason that option is disabled in the column creation screen.  Let's do this by returning to our 'Site Settings' menu and clicking 'Site columns':

image

Note our 'Clarity Asset Columns' grouping appears, with the manufacturer column underneath.  Click the Manufacturer column:

image

We see a similar