November 2007 - Posts
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:
In this dialog, we get the option to select an escalation and view its rule code:
Select this option, and we are presented with the Windows Workflow representation of this escalation rule:
We can double click the one code item in this workflow to bring us to its source code:
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:
Now we'll have to wait until the top of the hour to see if it automatically escalates to my task list.
*** Time Passes ***
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.
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':
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:
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:
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:
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:
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:
Click 'Next' and then 'Finish' to complete the wizard and your canvas should now look like this:
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:
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:
In the activity strip, select the 'Activity Escalations' icon, which resembles a little clock. This brings up the following Escalations dialog:
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:
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:
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:
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:
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:
Immediately after starting the process, we should see it on the service account's task list:
Now, we will wait five minutes and we should see it removed from the service account's task list:
and added to the 'real' user's task list (note the user names at the top of the screen shots):
I will now complete the task:
which will remove it from my task list:
And put it back on the service account's task list:
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.
Technorati Tags:
LINQ,
Visual Studio,
C#
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:
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).
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:
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:
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:
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:
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:
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:
Hold your breath, and check the Clarity Assets list...
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.
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':
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:
I will now walk through each step of the integration process:
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:
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:
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:
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:
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:
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:
Add and drag each of our Create methods:
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:
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:
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:
Would we like to configure a client event now? Sure! Check the box and click 'Finish' to start the InfoPath client event wizard:
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:
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:
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:
Click 'Next' and then 'Finish' to create the client event.
So here is what it gives us:
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:
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).
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:
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:
Repeat this process for Monitors and Hard Drives, so we have a total of three create methods:
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:
Great, so, we have our nicely defined Smart Object, what do we do with it now? That is a post for another day...
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':
Delete the default process that is created, so we have a nice empty environment:
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':
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:
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:

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:
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:
Click 'Next' and then 'Finish' to complete the creation of our method:
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:
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.
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':
Then select 'Custom List' to create:
Name the new list 'Asset Statuses' and ensure it gets placed on the Quick Launch menu:
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.:
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:
Click 'Site content types' to get back to our list of content types:
From the list of content types, click our 'Assets' content type which was created in the previous posts:
Here, we can add our new 'Asset Status' column via the 'Add from new site column' link:
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:
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:
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.
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?):
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:
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:
And your site columns gallery should look like this:
Now we need to create a list that will consume these content types. Go to 'Site Actions' - 'Create':
Select 'Custom List' from the create menu:
Name the list 'Clarity Assets', and for ease of use, make it available from the quick launch menu:
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':
In the 'Customize Clarity Assets' screen, select 'Advanced Settings':
Well well, looks like the very first option here deals with content types... 'Allow management of content types?' it asks? YES!
After clicking ok, we are brought back to the 'Customize Clarity Assets' screen, only this time there is a section for 'Content Types':
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:
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':
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:
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):
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':
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:
Now our list is looking a little more informative:
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':
Select 'All Items' to base this view on that one, it will give us all the default columns:
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:
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.
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':
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:
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:
Click the 'Create' button to bring up the 'New Site Content Type' screen. I will walk through each of the options presented here:
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:
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:
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':
Note our 'Clarity Asset Columns' grouping appears, with the manufacturer column underneath. Click the Manufacturer column:
We see a similar screen to the column creation screen, though now we can specify that the column is required by selecting 'Yes' to 'require that this column contain information':
Click 'Ok' to save the changes, and return to our site content type and add the remaining columns: Model and Serial Number in the same fashion as the Manufacturer column so that your content type looks like this:
Weird that the columns still say 'Optional' when I thought I made them required? It looks like that is an option that can override the default, so it is specific to this content type's implementation of the column. To make them required for this content type, we'll have to click the columns individually from the content type screen and make them required:
Do that for each column, and your content type should look like this:
So we have defined our base asset content type, next up is creating our Laptop, Monitor and Hard Drive content types that derive from it. After that, we need to create a list that implements these content types so we can see them in action.
So I thought about another scenario that I'd like to prove out using databinding in WPF: Lets create a databound combobox that displays a dropdown list of all possible states, selecting the state of the currently selected address. That was a long-winded way of saying: 'I want to choose the State of an address from a list'. Following the status-quo that we established in previous posts, lets create a 'StateCollection' and 'State' class, though we'll avoid all the notification and error handling stuff just for simplicity sake. The classes will look like so:
using System.Collections.ObjectModel;
namespace StumblingThroughWPFPartI
{
public class StateCollection : ObservableCollection<State>
{
public void LoadAll()
{
this.Add(new State("IL", "Illinois"));
this.Add(new State("CA", "California"));
}
}
}
using System;
using System.ComponentModel;
namespace StumblingThroughWPFPartI
{
public class State
{
String _abbreviation;
String _name;
public State(String abbreviation, String name)
{
_abbreviation = abbreviation;
_name = name;
}
public String Abbreviation
{
get
{
return _abbreviation;
}
set
{
_abbreviation = value;
}
}
public String Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
}
}
For the front-end, we will add a combobox named 'cboState' and a textbox named 'txtCity', positioning both of them next to our lbAddresses listbox. In fact, why don't we make things easier on ourselves and put them into a stackpanel, setting the stackpanel's DataContext to '{Binding Addresses}'. This will make binding our text box and combo box a bit easier, by putting them into the selected person's 'address' context. Simply bind the textbox to the 'City' property using the binding tag we used previously, the data context tells it that it is the address' property that is being bound. The combo box binding, though, is a bit trickier. I want it to display all state names, but have the values tied to address by state abbreviation. Before we get into that mess, we have to actually define and load our state collection instance. We'll do this just like we did our person collection, by including it in the dictionary like this:
<StumblingThroughWPFPartI:StateCollection x:Key="_allStates"/>
Then populating all states in the 'Load' event, found in the code behind:
((StateCollection)this.Resources["_allStates"]).LoadAll();
Now that we have that resource defined, we can get started on defining our combo box tag. First things first, we'll stick with what is familiar and define its 'ItemsSource', just like we did for the listbox (only specifying _allStates as the source):
ItemsSource="{StaticResource _allStates}"
Next, we tell it that we want it to display the state's name, again using a similar construct to the listbox:
DisplayMemberPath="Name"
Now things get interesting. I want the value behind each item in the list to be the state's abbreviation. To configure this, set the 'SelectedValuePath' property to the property of the bound object that represents the value, in this case, Abbreviation:
SelectedValuePath="Abbreviation"
Next, it is time to bind the selected value to the state of the address selected in the listbox. This can be achieved via the 'SelectedValue' property, by binding it to the 'State' property of our Addresses datacontext. Here is the logic to achieve this:
SelectedValue="{Binding State}"
Running the application and selecting a person shows us exactly what we had hoped:
We can also see that the 'State' dropdown is populated with all the states we defined in our State Collection:
We can prove the bindings are working by selecting the second guy, which we bound to San Francisco:
I think that concludes everything I want to do with databinding at this time. I am very happy with how it works in WPF, datacontext is incredibly useful as we saw with wrapping our address fields in a stackpanel with context. It is good to know that with all the 'look & feel' benefits of WPF, we aren't lacking in some of the nuts & bolts that real-world applications will still required. Here is the entirety of the window XAML as it stands now:
<Window x:Class="StumblingThroughWPFPartI.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:StumblingThroughWPFPartI="clr-namespace:StumblingThroughWPFPartI"
Title="Window1" Height="300" Width="559" Loaded="Window_Loaded">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/VisualFoundation;component/HotTrackListBox.xaml"/>
</ResourceDictionary.MergedDictionaries>
<StumblingThroughWPFPartI:PersonCollection x:Key="_personCollection"/>
<StumblingThroughWPFPartI:StateCollection x:Key="_allStates"/>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</Window.Resources>
<Window.Background>
<ImageBrush ImageSource="Greenstone.bmp" />
</Window.Background>
<Grid DataContext="{Binding Source={StaticResource _personCollection}}">
<ListBox Margin="22,73,0,89" Name="listBox1" FontSize="16" Style="{StaticResource HotTrackListBoxStyle}" DisplayMemberPath="FullName" ItemsSource="{Binding}" HorizontalAlignment="Left" Width="126" />
<TextBox Height="21" Margin="160,73,0,0" Name="txtFirstName" VerticalAlignment="Top" HorizontalAlignment="Left" Width="101">
<Binding Path="FirstName" ValidatesOnDataErrors="True"/>
</TextBox>
<TextBox Height="21" Margin="160,104,0,0" Name="txtLastName" VerticalAlignment="Top" HorizontalAlignment="Left" Width="101">
<Binding Path="LastName" ValidatesOnDataErrors="True"/>
</TextBox>
<ListBox HorizontalAlignment="Right" Margin="0,73,143,89" Name="lbPersonAddresses" Width="120" FontSize="16" Style="{StaticResource HotTrackListBoxStyle}" DisplayMemberPath="City" ItemsSource="{Binding Addresses}"/>
<StackPanel DataContext="{Binding Addresses}" Margin="0,73,0,110">
<TextBox Height="21" HorizontalAlignment="Right" Name="txtCity" VerticalAlignment="Top" Width="120" >
<Binding Path="City" ValidatesOnDataErrors="True"/>
</TextBox>
<ComboBox Height="21" HorizontalAlignment="Right" Name="cboState" VerticalAlignment="Top" Width="120" DisplayMemberPath="Name" SelectedValuePath="Abbreviation" SelectedValue="{Binding State}" ItemsSource="{StaticResource _allStates}" />
</StackPanel>
</Grid>
</Window>
We've left our project off with a listbox containing the full name of a bunch of people in a collection, and textboxes allowing us to update the first and last name of the selected person. The textboxes have some minimal validation in place, informing the user when their entry is invalid. What I promised I'd do next is create a hierarchical binding, adding addresses to each person. I'll start this task off by creating two classes similar to our PersonCollection and Person classes: AddressCollection and Address. We'll keep the class definitions simple, since the point of this exercise is to show the bindings not really to have a real-live application. So we'll give the address class two simple properties - City and State. The AddressCollection will need just one method - LoadByPerson - which accepts a person instance and will load the addresses for that person (presumably from a datasource, but we'll hardcode for now). The definition for each are as follows:
using System.Collections.ObjectModel;
namespace StumblingThroughWPFPartI
{
public class AddressCollection : ObservableCollection<Address>
{
public void LoadByPerson(Person parent)
{
if(parent.LastName.Equals("Guy"))
this.Add(new Address("Chicago", "IL"));
if (parent.LastName.Equals("One"))
this.Add(new Address("San Francisco", "CA"));
}
}
}
using System;
using System.ComponentModel;
namespace StumblingThroughWPFPartI
{
public class Address : INotifyPropertyChanged, IDataErrorInfo
{
String _city;
String _state;
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
{
PropertyChanged(this, e);
}
public Address(String city, String state)
{
_city = city;
_state = state;
}
public String City
{
get
{
return _city;
}
set
{
_city = value;
}
}
public String State
{
get
{
return _state;
}
set
{
_state = value;
}
}
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;
}
}
}
}
Now the 'Person' class needs a way to retrieve its addresses. We'll create a read-only property that will load up an address collection (if one hasn't already been loaded) and return it to the caller. That property would look like this (note that an AddressCollection module-level variable named '_addresses' needs to be declared at the top of the class too):
public AddressCollection Addresses
{
get
{
if (this._addresses == null)
{
this._addresses = new AddressCollection();
_addresses.LoadByPerson(this);
}
return this._addresses;
}
}
Now our 'business objects' are set up, so we need to update our interface to include another listbox, which will show the addresses of the selected person. Simply drag another list box out and place it to the right of the textboxes, so it looks like this (increase the width of the window as necessary):
Name the listbox lbPersonAddresses. Now how do we setup its bindings to get the current person's addresses? First off, lets set the DisplayMemberPath to "City" and ensure that the style matches the other listbox. Now, simply set the ItemSource = {Binding Addresses}. Remember that this listbox is a part of a grid where the data context was set to our person collection. Because of this, we only need to specify which property of the person collection we want to bind the source to. Since the 'Addresses' property returns a collection, it is a perfectly valid choice for the ItemSource property. Running our project now yields these results:
And clicking a different person yields these results:
Well that was easy! I need to think about how else we can challenge databinding in WPF, as so far it is as easy as in WinForms (2005) just without the fancy visual interface (which I am assured comes with Expression Blend, a product I have yet to try). Here is the XAML tag for the new listbox:
<ListBox HorizontalAlignment="Right" Margin="0,73,8,89" Name="lbPersonAddresses" Width="120" FontSize="16" Style="{StaticResource HotTrackListBoxStyle}" DisplayMemberPath="City" ItemsSource="{Binding Addresses}"/>
Our project now has a listbox bound to the full name of a collection of people, and textboxes that allow the user to update the first or last name of the selected person and have that change reflected in the listbox. What I'd like to add now is a way to prevent the user from clearing out the first and last name, basically validating that the length of the first and last name is greater than zero and displaying an appropriate message if it is not. Additionally, I'd like the validation logic to reside within the person class itself; the form shouldn't have to worry about what is a valid value to put into a person's name. The key to this functionality lies in the 'IDataErrorInfo' interface, which resides in the System.ComponentModel namespace. When this interface is implemented in a bound object, it provides the functionality to communicate errors to the front-end. All we need to do to implement this interface is to provide the logic for an 'Error' property (returns the message for when anything in the object is invalid) and a default property with a 'name' parameter, which is the error message for the property with the given name. There are a number of different ways to implement these methods, they aren't new to WPF so I'm not going to cover them in detail here. All I am going to do is hardcode a little something for the error message by property name and leave the generic error as null. It will look a little something like this:
public string Error
{
get
{
return null;
}
}
public string this[string name]
{
get
{
string result = null;
if (name.Equals("FirstName"))
{
if (this._firstName.Trim().Length == 0)
result = "Please specify a first name";
}
else if (name.Equals("LastName"))
{
if (this._lastName.Trim().Length == 0)
result = "Please specify a last name";
}
return result;
}
}
Let's just take a shot in the dark here and run the project, maybe it'll just work! Er... no, it doesn't. In fact, if I put a breakpoint in either of these new IDataErrorInfo properties, we see that the code path never even reaches these guys. What gives? How do we tell our window to try and use the Data Error Info? I'm sad to find out that the answer to this question involves changing the way I bound the text boxes to the object. In order for a binding to actively 'listen' for data errors, the 'ValidatesOnDataErrors' property must be set to true. Since this property is a part of the binding, there is no way to set it from our TextBox tag as it is currently defined... we'll need to set up the binding tag as a child of our textbox tags, like this:
<TextBox Height="21" HorizontalAlignment="Right" Margin="0,73,10,0" Name="txtFirstName" VerticalAlignment="Top" Width="120" >
<Binding Path="FirstName" ValidatesOnDataErrors="True"/>
</TextBox>
<TextBox Height="21" HorizontalAlignment="Right" Margin="0,104,10,0" Name="txtLastName" VerticalAlignment="Top" Width="120" >
<Binding Path="FirstName" ValidatesOnDataErrors="True"/>
</TextBox>
I guess that's ok, it is fairly logical and maybe we can use a style in some way so that ValidatesOnDataErrors is always set to true, kind of like that 'IsSynchronizedWithCurrentItem' property of the listbox discussed in a previous post. If we run the application now, and enter in a blank for first or last name, we see the following:
Not Incredibly informative, but at least it knows that the field had an error. I'd like to at least display the error message in a tool tip so the user can see what is wrong (and we can verify our validation logic works). To change the tooltip when a validation error occurs, we can resort back to triggers, which can conveniently handle the 'HasError' event that gets raised by our object. The following XAML will apply a style to all textboxes that will display the error message in its tooltip whenever the HasError event is handled:
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
Now when we run the application and enter a blank first name, we get this:
The screenshot got cut-off a bit, but it does say 'Please specify a first name', which is the error message we provided. Here is the full XAML of our window as it exists now:
<Window x:Class="StumblingThroughWPFPartI.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:StumblingThroughWPFPartI="clr-namespace:StumblingThroughWPFPartI"
Title="Window1" Height="300" Width="300" Loaded="Window_Loaded">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/VisualFoundation;component/HotTrackListBox.xaml"/>
</ResourceDictionary.MergedDictionaries>
<StumblingThroughWPFPartI:PersonCollection x:Key="_personCollection"/>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</Window.Resources>
<Window.Background>
<ImageBrush ImageSource="Greenstone.bmp" />
</Window.Background>
<Grid DataContext="{Binding Source={StaticResource _personCollection}}">
<ListBox Margin="22,73,136,89" Name="listBox1" FontSize="16" Style="{StaticResource HotTrackListBoxStyle}" DisplayMemberPath="FullName" ItemsSource="{Binding}" />
<TextBox Height="21" HorizontalAlignment="Right" Margin="0,73,10,0" Name="txtFirstName" VerticalAlignment="Top" Width="120" >
<Binding Path="FirstName" ValidatesOnDataErrors="True"/>
</TextBox>
<TextBox Height="21" HorizontalAlignment="Right" Margin="0,104,10,0" Name="txtLastName" VerticalAlignment="Top" Width="120" >
<Binding Path="FirstName" ValidatesOnDataErrors="True"/>
</TextBox>
</Grid>
</Window>
And here is the person class:
using System;
using System.ComponentModel;
namespace StumblingThroughWPFPartI
{
public class Person : INotifyPropertyChanged, IDataErrorInfo
{
String _firstName;
String _lastName;
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
{
PropertyChanged(this, e);
}
public Person(String firstName, String lastName)
{
_firstName = firstName;
_lastName = lastName;
}
public String FirstName
{
get
{
return _firstName;
}
set
{
_firstName = value;
this.OnPropertyChanged(new PropertyChangedEventArgs("FullName"));
}
}
public String LastName
{
get
{
return _lastName;
}
set
{
_lastName = value;
this.OnPropertyChanged(new PropertyChangedEventArgs("FullName"));
}
}
public String FullName
{
get
{
return _firstName + " " + _lastName;
}
}
public string Error
{
get
{
return null;
}
}
public string this[string name]
{
get
{
string result = null;
if (name.Equals("FirstName"))
{
if (this._firstName.Trim().Length == 0)
result = "Please specify a first name";
}
else if (name.Equals("LastName"))
{
if (this._lastName.Trim().Length == 0)
result = "Please specify a last name";
}
return result;
}
}
}
}
Next up, hierarchical data binding. I am going to add an 'Address' class and collection, where each person can have zero to many addresses associated with them. I'll display their addresses in another listbox as the people get selected from the current listbox... at least, that's my goal.
We left off previously with a listbox bound to a custom collection of People, showing their first name in the list. What I want to do now is drop two textboxes onto the window that display the first and last name of the person selected in the listbox. If the user manipulates the first name in the text box, the change should be reflected in the listbox as well. First things first, lets drag out two text boxes from the toolbox and place them next to the list box on the window. We're not going to worry about aesthetics here, just get them out there anywhere and name them, oh I dunno, txtFirstName and txtLastName. For each of these textboxes, we want to bind their 'Text' property to the corresponding property of the PersonCollection dictionary. Here is the line that will do it for FirstName, it can be copied and pasted for LastName as well (just change the path):
Text="{Binding Source={StaticResource _personCollection},Path=FirstName}"
Similar to how we bound the listbox itemsource, this line is telling the textbox to get its 'Text' value from the dictionary object with a key of _personCollection. We additionally get the option to specify the binding property name (Path) all in the same line. This syntax feels a bit ugly to me, and still suffers from one of the biggest drawbacks that databinding has always exhibited, that is, if the property name being bound to ever changes, the code will still compile but fail at run time because the bound property name is essentially hard-coded, and not tied in any way contextually to the binding source. I'm told by local experts that using expression blend will help alleviate this problem, so I'm not going to make too big of a deal of it right now, but I will re-visit the syntax later and see if we can make it neater. As it is, when we run the app and select a person from the list, we can see the text boxes reflecting the person's first and last names:
If we change the first name via the first name text box and tab off of it, we see that the first name in the listbox is automatically updated:
Great! Next order of business is data validation combined with data binding, and hierarchical databinding too. We'll make it so that first and last name cannot be specified as blank in the text box, or the textbox will glow red. We will also link a Person to a collection of addresses, so another listbox will appear that displays the addresses of the selected person.
That is a beautiful thing, and I believe it comes from using the 'ObservableCollection' as our base collection class. One very very important item I need to note here is that, if you are using my 'HotTrackListBoxStyle' for the listbox, you'll recall that I set a property of the listbox IsSynchronizedWithCurrentItem to true. If you do not set this property, then the bindings to the textboxes WILL NOT WORK! I can't tell you how many hours I wasted trying to figure out what was wrong with my bindings, that is why I put that property setter in the style so I will never forget to set it again (assuming I always use that ListBox style).
Now I'd like to revisit the syntax for our databinding... it seemed a little redundant to me to specify the object and path for every control, when the object is always the same. This problem would only be magnified as we add more controls, so lets address it now with a little thing called 'DataContext'. This property is available on all container controls (such as grids and stackpanels) and allows you to specify root bindings for all controls in said container. In our little example, we know that all controls in the grid (the listbox and two textboxes) are all sourced from the _personCollection dictionary, so if we were to set the DataContext of the grid, then we should be able to clean up the databinding syntax of the controls. Set up the main Grid tag to look like this:
<Grid DataContext="{Binding Source={StaticResource _personCollection}}">
Now, each of our controls in that grid no longer have to specify the _personCollection binding source, simply stating 'Binding' implies the same binding of the container's data context. So now, our ItemSource for the listbox can look like this:
ItemsSource="{Binding}"
And the Text binding for the textboxes can look like this:
Text="{Binding Path=FirstName}"
I like that syntax much better, and its true power will be more evident when you are binding to many different collections on the same form, and you can group your controls based on data context to keep things nice and tidy.
I have one other task to attempt here that might get interesting... Let's make a new property of our Person class called 'FullName', which is read-only and concatenates the first and last name of the person. It will look like this:
public String FullName
{
get
{
return _firstName + " " + _lastName;
}
}
Now bind the listbox's displaymember to be the full name instead of the first name:
Easy, right? However, if we now change the first name via textbox, the listbox value is not updated to reflect our change. How do we tell the listbox that the full name has changed? Similar to WinForms databinding, we'll need our class to implement the INotifyPropertyChanged interface, which forces us to specify the 'PropertyChanged' event and OnPropertyChanged method (which simply raises the PropertyChanged event with the appropriate arguments). When we have these items defined, then we can call OnPropertyChanged when the first or last name is changed, telling it that 'FullName' has also changed. The following is the full definition of the Person object that will do just that:
namespace StumblingThroughWPFPartI
{
public class Person : System.ComponentModel.INotifyPropertyChanged
{
String _firstName;
String _lastName;
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
{
PropertyChanged(this, e);
}
public Person(String firstName, String lastName)
{
_firstName = firstName;
_lastName = lastName;
}
public String FirstName
{
get
{
return _firstName;
}
set
{
_firstName = value;
this.OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("FullName"));
}
}
public String LastName
{
get
{
return _lastName;
}
set
{
_lastName = value;
this.OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("FullName"));
}
}
public String FullName
{
get
{
return _firstName + " " + _lastName;
}
}
}
}
Now when we run the application and change either the first or last name, the full name displayed in the listbox is automatically updated:

I have a love-hate relationship with databinding. I believe it was first introduced to me very early in my career, either in some incarnation of Visual Basic (4, perhaps?) or in the language of choice for my first job out of college: Centura Team Builder (kudos to anyone who has ever heard of that language). Wherever I heard about it, I remember falling on it like a starving man on a roast beef sandwich. I thought it was the coolest thing ever, and I told everyone how it is going to make all of our data-driven interface logic completely obsolete. Well, to make a long story short, I never could get data binding to do everything I wanted as easily as I wanted it, and it fell out of favor. With every new version of Visual Basic and then Visual Studio .NET, I always leapt back into their 'new and improved' databinding until I ultimately left disappointed yet again. I finally jumped back on board with databinding for good in Visual Studio 2005, where I think they finally got it right (or at least close enough for me). Seeing as though WPF is built on or around the same framework, its databinding capabilities should be at least as good as Win Forms in 2005, right? Well, that is what I am going to figure out by stumbling through databinding in WPF.
The first order of business is setting up something to databind TO. I'm going to use the same project from my previous posts, which now consists of a stylized, transparent listbox on a window with a nice stone background hijacked from some generic windows desktop bitmap. Since I am a strong proponent of the whole binding-to-collections way of thinking, I'm going to create two classes: PersonCollection and Person. Person will be made up of first and last name properties, while PersonCollection will have a 'LoadAll' method to add three person instances to its underlying collection. Now something that I haven't mentioned yet is which 'collection base' should be used as the base class for our PersonCollection class. Instead of one of the traditional base collection classes, we will be using the 'ObservableCollection' class, which is supposed to include support for property changed and validation events. We'll investigate what this does for us when we start consuming this object. This class is a part of the System.Collections.ObjectModel namespace, and is generic so we'll have to specify the type of object it will contain (Person).
Here is the source for the Person class:
namespace StumblingThroughWPFPartI
{
public class Person
{
String _firstName;
String _lastName;
public Person(String firstName, String lastName)
{
_firstName = firstName;
_lastName = lastName;
}
public String FirstName
{
get
{
return _firstName;
}
set
{
_firstName = value;
}
}
public String LastName
{
get
{
return _lastName;
}
set
{
_lastName = value;
}
}
}
}
Here is the source for the PersonCollection class:
namespace StumblingThroughWPFPartI
{
public class PersonCollection : ObservableCollection<Person>
{
public void LoadAll()
{
this.Add(new Person("Test", "Guy"));
this.Add(new Person("Another", "One"));
this.Add(new Person("Someone", "Else"));
}
}
}
As you can see by the class definitions, they are a part of the 'StumblingThroughWPFPartI' namespace. Our test window, where we will be using these classes, has no idea about that namespace so we need to register it at the top of the page like this:
xmlns:StumblingThroughWPFPartI="clr-namespace:StumblingThroughWPFPartI"
Now we can instantiate our PersonCollection object by adding it to the resource dictionary like this:
<StumblingThroughWPFPartI:PersonCollection x:Key="_personCollection"/>
Defining our instance this way automatically instantiates the class as an empty Person Collection and puts it into the resource dictionary with a key of '_personCollection'. To load up our collection, we need to access it in the code behind to invoke its 'LoadAll' method. Following my WinForms inutition, I attempt to double click the form to bring up its 'Load' event handler, thinking I'd just populate the collection in the load event. Cool, not only did it bring me to the 'Window_Loaded' event handler, it also threw something in my XAML that tells it where do go for the 'Loaded' event. This attribute is a part of the Window tag:
Loaded="Window_Loaded"
Now we can code up our Window_Loaded event handler. The first order of business is to retrieve our Person Collection from the resource dictionary. This can be achieved in the following line:
(PersonCollection)this.Resources["_personCollection"];
We can further enhance this line, since we aren't worrying about error handling at this time, to load our object as well:
((PersonCollection)this.Resources["_personCollection"]).LoadAll();
So now we have our fully loaded Person Collection in the resource dictionary, how do we bind our list box to display the first name of every person? Back in our XAML, lets remove any ListBoxItem tags that may still be there from the previous posts. In our empty list box, I'm looking for two properties: Where is the data source, and where is the Display Member? Display Member is there as DisplayMemberPath, calling it a 'path' makes me think you can bind into nested hierarchies but I'm not going to think about that yet. I'm just setting that property to 'FirstName'. The only thing I can find that is roughly equivilent to Data Source is 'ItemSource'. What I've seen of the syntax for this property seems pretty funky... I know that in order for it to do what we want, it needs to look like this:
ItemsSource="{Binding Source={StaticResource _personCollection}}"
My interpretation of this is that we are sending ItemSource equal to a new binding, whose source is the current dictionary instance of the item with the '_personCollection' key. Running the project has the expected results:
Next, I'm going to try and bind the selected first name to a text box, allow editing and maybe even force validation.
More Posts
Next page »