Social

fredag den 30. maj 2014

Lend out IT-equipment in Service Manager using custom forms and console tasks - Part 2a

In this part I will be discussing console tasks that will allow a console operator to lend out an item as well as return it.

In order to expose the console task to the console we will need a MP telling Service Manager the necessary details. First off we will define when a console task should be shown. When using a console task in a form (a so called FormTask) we have access to an interface called IDataItem. Changes made using this interface will reflect immediately in the form (and we will not have to bother with saving the changes).
When calling a console task from a view we will be editing an EnterpriseManagementObject (or some variant thereof).

First of I will limit the console tasks to only work from a view:

<Category ID="LendItemTaskHandler.DonotShowFormTask.Category" Target="LendItemTaskHandler" Value="Console!Microsoft.EnterpriseManagement.ServiceManager.UI.Console.DonotShowFormTask" />
<Category ID="ReturnItemTaskHandler.DonotShowFormTask.Category" Target="ReturnItemTaskHandler" Value="Console!Microsoft.EnterpriseManagement.ServiceManager.UI.Console.DonotShowFormTask" />

Next we define the console tasks. I will just show the code for the first one. The ID is the target we defined above. The target of the console task is then defined as a class just like when doing type projections.
What we are doing is telling the Microsoft.EnterpriseManagement.UI.SdkDataAccess.ConsoleTaskHandler that it should invoke CB.LendableItemConsoleTasks (this is the name of the assembly, the DLL-file) when someone clicks the task in the console, and type is a combination of the namespace the LendableTaskHandler is contained in, ie. namespace is CB.LendableItemConsoleTasks in which a class LendableTaskHandler is defined, and finally we provide a single argument "LendItem" which we can look for in the code later on.

<ConsoleTask ID="LendItemTaskHandler" Accessibility="Public" Enabled="true" Target="LendableLibrary!CB.LendableItem" RequireOutput="false">
<Assembly>Console!SdkDataAccessAssembly</Assembly>
<Handler>Microsoft.EnterpriseManagement.UI.SdkDataAccess.ConsoleTaskHandler</Handler>
<Parameters>
  <Argument Name="Assembly">CB.LendableItem.ConsoleTasks</Argument>
  <Argument Name="Type">CB.LendableItem.TaskHandlers.LendableTaskHandler</Argument>
  <Argument>LendItem</Argument>
</Parameters>
</ConsoleTask>

The entire XML can be viewed here.

Next up is adding an empty project to the solution in which the custom form is. We call the project CB.LendableItem.ConsoleTasks (this will also be the name of the DLL). Go to project properties and change the output type to "class library" and make sure the target framework is .NET Framework 3.5. Optionably you can also sign the assembly in the signing tab - the console will complain if executing console tasks from an unsigned assembly.

In order to avoid writing the same code over and over again when creating console tasks I use inheritance:

    class TaskHandler : ConsoleCommand
    {
        private IDataItem _dataItem;
        private EnterpriseManagementObject _emo;
        EnterpriseManagementObjectProjection _emop;
        private EnterpriseManagementGroup _mg;

        public override void ExecuteCommand(IList<NavigationModelNodeBase> nodes, NavigationModelNodeTask task, ICollection<string> parameters)
        {
            base.ExecuteCommand(nodes, task, parameters);

            NavigationModelNodeBase node = nodes.First();

            //Get the server name to connect to
            String strServerName = Registry.GetValue("HKEY_CURRENT_USER\\Software\\Microsoft\\System Center\\2010\\Service Manager\\Console\\User Settings", "SDKServiceMachine", "localhost").ToString();

            //Connect to the server
            _mg = new EnterpriseManagementGroup(strServerName);


            if (nodes[0] is EnterpriseManagementObjectNode)
            {
                _emo = (nodes[0] as EnterpriseManagementObjectNode).SDKObject;
            }
            else if (nodes[0] is EnterpriseManagementObjectProjectionNode)
            {
                _emop = (EnterpriseManagementObjectProjection)(nodes[0] as EnterpriseManagementObjectProjectionNode).SDKObject;
                _emo = _emop.Object;
            }

            _dataItem = Microsoft.EnterpriseManagement.GenericForm.FormUtilities.Instance.GetFormDataContext(node);
        }

        public IDataItem DataItem
        {
            get
            {
                return _dataItem;
            }
        }

        public EnterpriseManagementObject ManagementObject
        {
            get
            {
                return _emo;
            }
        }

        public EnterpriseManagementObjectProjection ManagementObjectProjection
        {
            get
            {
                return _emop;
            }
        }

        public EnterpriseManagementGroup ManagementGroup
        {
            get
            {
                return _mg;
            }
        }
    }

What I have done here is create a generic TaskHandler. I can then simply inherit it like this

    class LendableTaskHandler : TaskHandler
    {
        // variables go here

        public override void ExecuteCommand(IList<NavigationModelNodeBase> nodes, NavigationModelNodeTask task, ICollection<string> parameters)
        {
            base.ExecuteCommand(nodes, task, parameters);

And get on with the code specific for this console task. Before we continue we need to make sure we have a proper object projection in which we can access ex. the user who borrowed an item.

// search criteria for ObjectProjectionCriteria
String sId = ManagementObject[mpLendableItemLibrary.GetClass("CB.LendableItem"), "CB_ItemID"].Value.ToString();
String sLendableItemSearchCriteria = "";
sLendableItemSearchCriteria = String.Format(@"<Criteria xmlns=""http://Microsoft.EnterpriseManagement.Core.Criteria/"">" +
                "<Expression>" +
                "<SimpleExpression>" +
                    "<ValueExpressionLeft>" +
                    "<Property>$Context/Property[Type='CB.LendableItem']/CB_ItemID$</Property>" +
                    "</ValueExpressionLeft>" +
                    "<Operator>Equal</Operator>" +
                    "<ValueExpressionRight>" +
                    "<Value>" + sId + "</Value>" +
                    "</ValueExpressionRight>" +
                "</SimpleExpression>" +
                "</Expression>" +
            "</Criteria>");

ManagementPackTypeProjection mptpLendable = mpLendableItemLibrary.GetTypeProjection("TypeProjection.LendableItem");

ObjectProjectionCriteria opcLendable = new ObjectProjectionCriteria(sLendableItemSearchCriteria, mptpLendable, mpLendableItemLibrary, ManagementGroup);

IObjectProjectionReader<EnterpriseManagementObject> oprLendables =
    ManagementGroup.EntityObjects.GetObjectProjectionReader<EnterpriseManagementObject>(opcLendable, ObjectQueryOptions.Default);

_emop = oprLendables.First();

This is based on something Travis posted. In short we retrieve the item already provided to use in ExecuteCommand, but with the necessary type projections.

Remember the argument provided in the xml ealier? It can be accessed like this

if(parameters.Contains("LendItem"))
{
    LendItem();
}
else if(parameters.Contains("ReturnItem"))
{
    ReturnItem();
}

RequestViewRefresh();

, and when either of those two methods are done executing we refresh the view.

I will also setup some helper functions

public EnterpriseManagementSimpleObject GetCurrentStatus()
{
    return ManagementObject[mpcLendableItem, "CB_Status"];
}

I will be looking up the current status alot. mpcLendableItem is defined in ExecuteCommand, and ManagementObject in the parent ExecuteCommand (the generic one).

I will also be in need of retrieving related users, such as the user who reserved the item

public EnterpriseManagementObject GetReservedByUser()
{
    ManagementPackRelationship mprReservedBy = mpLendableItemLibrary.GetRelationship("CB_ReservedBy");

    foreach (EnterpriseManagementRelationshipObject<EnterpriseManagementObject> obj in
        ManagementGroup.EntityObjects.GetRelationshipObjectsWhereSource<EnterpriseManagementObject>(ManagementObject.Id, TraversalDepth.OneLevel, ObjectQueryOptions.Default))
    {
        if (obj.RelationshipId == mprReservedBy.Id)
            return obj.TargetObject;
    }
    return null;
}

This is just an altered code snippet from Rob Ford.

Now let's get on with lending out an item. First I will be validating that the item is actually lendable, ie. someone reserved it, and the status is 'Reserved'.

EnterpriseManagementSimpleObject currentStatusEMO = GetCurrentStatus();
EnterpriseManagementObject reservedBy = GetReservedByUser();

if (reservedBy != null && currentStatusEMO.ToString().Equals(mpEnumReserved.ToString()))
{

I am already using the helper functions! See this post on comparing enumerations.

Next we will be creating a 'borrowed' relationship between the user who reserved the item and the item.

EnterpriseManagementObjectProjection projection = ManagementObjectProjection;

ManagementPackRelationship mprBorrowedBy = mpLendableItemLibrary.GetRelationship("CB_BorrowedBy");

projection.Add(reservedBy, mprBorrowedBy.Target);

So we simply retrieve the projection defined earlier in this post and then add the relationship. Note that the relationship is defined as

<RelationshipType ID="CB_BorrowedBy" Accessibility="Public" Abstract="false" Base="System!System.Reference">
  <Source ID="Source_bad06373_9362_433d_be2f_adf7aa2b5912" MinCardinality="0" MaxCardinality="2147483647" Type="CB.LendableItem" />
  <Target ID="Target_87f8bbbd_5aba_4013_aaf1_b2f15c00addc" MinCardinality="0" MaxCardinality="1" Type="MicrosoftWindowsLibrary!Microsoft.AD.User" />
</RelationshipType>

which is why we use mprBorrowedBy.Target and not mprBorrowedBy.Source.

In order to avoid commit clashing (calling commit on the same object in succession) properties in the projection is entered as

DateTime now = DateTime.Now;
projection.Object[mpLendableItemLibrary.GetClass("CB.LendableItem"), "CB_BorrowedDate"].Value = now;

// must be returned within 28 days
projection.Object[mpLendableItemLibrary.GetClass("CB.LendableItem"), "CB_ReturnDate"].Value = now.AddDays(28);

// status is now borrowed
projection.Object[mpLendableItemLibrary.GetClass("CB.LendableItem"), "CB_Status"].Value = mpEnumBorrowed;

// commit on projection will also commit the object
projection.Commit();

Return item is somewhat similar, except that we need to remove some relationships. What I ended up with

EnterpriseManagementSimpleObject currentStatusEMO = GetCurrentStatus();
EnterpriseManagementObject borrowedBy = GetBorrowedByUser();

if (borrowedBy != null && currentStatusEMO.ToString().Equals(mpEnumBorrowed.ToString()))
{  
    ManagementPackRelationship mprReservedBy = mpLendableItemLibrary.GetRelationship("CB_ReservedBy");
    ManagementPackRelationship mprBorrowedBy = mpLendableItemLibrary.GetRelationship("CB_BorrowedBy");

// Remove the related users     (ManagementObjectProjection[mprReservedBy.Target].First() as IComposableProjection).Remove();
    (ManagementObjectProjection[mprBorrowedBy.Target].First() as IComposableProjection).Remove();

    ManagementObjectProjection.Object[mpLendableItemLibrary.GetClass("CB.LendableItem"), "CB_BorrowedDate"].Value = null;
    ManagementObjectProjection.Object[mpLendableItemLibrary.GetClass("CB.LendableItem"), "CB_ReservedDate"].Value = null;
    ManagementObjectProjection.Object[mpLendableItemLibrary.GetClass("CB.LendableItem"), "CB_ReturnDate"].Value = null;
    ManagementObjectProjection.Object[mpLendableItemLibrary.GetClass("CB.LendableItem"), "CB_Status"].Value = mpEnumAvailable;

    ManagementObjectProjection.Commit();


In part 2b I will be adding an offering on the portal allowing a user to reserve the item. I may also elaborate abit on the current solution (ex. returning items in bulks).

Full source-code available here.




onsdag den 28. maj 2014

Comparing enumeration values in a Service Manager Console Task

While working on part 2 of my adventure into Service Manager customization I came across a seemingly simple problem; comparing two enumeration values. I wanted to change the value bound to a custom configuration item if it had a specific value, and it seemed a bit lackluster to compare the DisplayNames of the two. I would rather compare GUIDs or something similarly unique.
After spending ages on figuring out how to get the GUID out of the bound enumeration value I found that the toString method actually provided me with what I needed.

First a bit of setup. I need the MP that defines the custom class I made

ManagementPack mpLendableItemLibrary = ManagementGroup.ManagementPacks.GetManagementPack(new Guid("370a302c-9b0c-6c1a-033d-9b97f8406db5")); 

I also need the instance as an EnterpriseManagementObject. In the ExecuteCommand a list of nodes is provided (containing as many nodes as selected in a view, or just one in a form). Thus I can get it by

managementObject = node["$EMOInstance$"] as EnterpriseManagementObject

Or (Suggested by Rob Ford)

if (node is EnterpriseManagementObjectNode)
{
    managementObject = (node as EnterpriseManagementObjectNode).SDKObject;
}
else if (node is EnterpriseManagementObjectProjectionNode)
{
    EnterpriseManagementObjectProjection emop = (EnterpriseManagementObjectProjection)(node as EnterpriseManagementObjectProjectionNode).SDKObject;
    managementObject = emop.Object;
}

I actually couldn't use the instance of IDataItem to do the comparison. The enumeration is defined in the same MP as the custom class.

ManagementPackEnumeration mpEnumBorrowed =
    mpLendableItemLibrary.GetEnumerations().GetItem("Enum.Borrowed");

I can get the property CB_Status by

EnterpriseManagementSimpleObject currentStatusEMO = managementObject [mpLendableItemLibrary.GetClass("CB.LendableItem"), "CB_Status"];

And finally I can do the comparison (both ToString methods provide me with the name of the enumeration which also must be unique).

if(currentStatusEMO.ToString().Equals(mpEnumBorrowed.ToString()))
{
    // do something!



torsdag den 15. maj 2014

Lend out IT-equipment in Service Manager using custom forms and console tasks - Part 1

Inspired by John Hennens Building Custom Forms for Service Manager with Visual Studio I will give an example that is somewhat closer to a reallife Service Manager customization. Many IT departments lend out equipment to employees. One could use something like a service request to keep track of who has borrowed what, and besides the fact that a service request shouldn't be long lived by design, a seperate system (be that post-it notes or something more advanced) is needed to keep track of the equipment. So what we wish from a Service Manager customization is
  1. Users can browse and reserve equipment on the self service portal
  2. Items can be managed in the console (details will follow)
First we will create a new custom class based on the configuration item class with the following properties and relationships:
  • Borrowed Date - Datetime - The date the item was borrowed by a user
  • Reserved Date - Datetime - The date the item was reserved by a user
  • Return Date - Datetime - The date the item must be returned by a user
  • Status - List - A list of the different states an item can be in, Available, Reserved, Borrowed, Overdue
  • Reserved by - Relationship - The user the item is reserved by
  • Borrowed by - Relationship - The user the item is borrowed by
We then create a type projection exposing these two relationships allowing us to easily access these when building the form.

We need a custom form that can display all of these properties (and more), and console tasks to manage them:
  • Borrow item - Changes the status to 'Borrowed' and updates the 'Borrowed by' relationship.
  • Return item - Changes the status to 'Available' and deletes the 'Borrowed by' relationship.
  • Reset item - Sets all properties to default values and removes relationships.
We also need a runbook that reserves the requested item. For the sake of it I will be using SMA (or die trying).

Enough talk, more action! First I created the custom form. You can view the entire XAML-code here.

It looks like this btw:


It seems that there is currently a bug in WPFToolkit where the Datepicker resides which makes the "Show Calendar" button looks greyed out as if it was disabled.
Edit:
This is supposedly a fix to the issue, but the datepickers are still wrong in my implementation. Bummer :(

We will using the form primarily for viewing and not editing. For this purpose I will create a few console tasks.

As explained by John one will need to target the custom form at a type projection in order to access class relationships directly using XAML. The class definition is described here, along with type projections and values for the status enumeration.
The custom form is defined in XML here. Note that I have signed the assembly using the same key as I use for signing management packs. This can be done in Visual Studio in properties for a project in the signing tab. Check the "Sign the assembly" box and select the key to sign with. I am also signing all MPs except the one containing views.

All source code can be found here, and a ready to import MP-bundle here.

In part 2 I will be doing console tasks and putting an offering on the portal for end-users to request reservation of an item. In part 3 I will attempt at adding an easy to view history to the custom form that shows who reserved or borrowed an item in the past.

Update:
I just realized I was not using a UserPicker, the obvious choice for picking users, DOH! Simply use this code in place of the SingleInstancePicker
<scwpf:UserPicker User="{Binding Path=IsReservedBy, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

torsdag den 1. maj 2014

Subscribing to Review Activities - Avoid a Common Pitfall

Setting up a subscription in Service Manager is easy. In many setups something similar to "Automatically Sending Notifications to Reviewers" is configured, or even better "Tricky Way to Handle Review Activity Approvals with the Exchange Connector". But sometimes there are no reviewers for a review activity. The workflow/subscription runs regardles, and when later someone spots the lack of reviewer it is too late - You can use one of my previous posts to configure a view that will show all review activities (or even service requests that have review activities) that is missing a reviewer.

The solution is simple. This won't work with workflows though (they cannot access the related reviewer objects). I much prefer subscriptions and the "Periodically notify when objects meet a criteria" and then select "notify once" in the recurrence pattern. Note: the subscription will not trigger again if someone "returns to activity" - the "is updated" subscription does not have access to the reviewer relationship.

Add an additional criteria that the displayname property of the reviewer object is not empty. What can happen is that the service requests affected user has no manager, and the "Line manager should review" checkbox is checked in the review activity. What Service Manager erroneously does is to create the reviewer object (which is not the actual reviewing user) and a relationship between this and the review activity. It then tries to relate the user (system.user) object which doesn't exists as there is no manager. The DisplayName property is incidently the DisplayName of the user which does not exists and is there the empty string (technically I believe it to be null).

The subscription will then trigger when a reviewer is eventually added to the review activity.

I will also share a tip to solve the issue of review activities that you do not want to subscribe to. Maybe a specific mail should be send, or no mail at all. What I do is to add yet another criteria to the general purporse subscription along the lines of "SomeCustomField" is empty or contains a specific value (like "GeneralPurposeReviewActivitySubscriptionEnabled").

Now that you have fixed the problem on your end talk to your AD-team. If the problem is not of a technical nature, but rather an issue with your organization, you may be able to get a hold of a list of sorts indicating who should act as manager for specific users. A runbook could help you sort this out automatically

  1. Monitor reviewers where DisplayName is empty (you may need to use a regex, I think the empty string would be: ^$
  2. Get the related review activity
  3. Get the affected user of that review activity
  4. look up the acting manager for that user (list, db, AD logic)
  5. Get the acting manager as a user object
  6. Create a relationship between the reviewer object and the acting manager user object

Søg i denne blog