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.




Ingen kommentarer:

Send en kommentar

Søg i denne blog