How to add custom mandatory properties to SCSM forms

Description

I get asked very often how to make properties on SCSM forms mandatory. It requires customisation of the form concerned and also some C# coding, but it is not too difficult to achieve. In this post, I will show you how to make Description on the Incident Form required as an example.

This example is basically a rework of my post on disabling form controls here. If you used this solution, this will be very easy for you to do :)

Validation in SCSM is done via use of WPF ValidationRules. More on these can be found here.

Firstly, create a new WPF User Control Library in Visual Studio targeting the .NET Framework 3.5:

CreateProject

It is very important to target the 3.5 version of the framework. Your control will not work if you use a newer version!

Add the following references to Service Manager assemblies:

Refs

Not all of these are required for this example, but if you want to extend this solution to include adding validation to SCSM controls (such as UserPickers) you will need these.

Add a new folder called “Classes” and a new class called “Validation.cs”:

Classes

Replace all of the code in Validation.cs with this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Globalization;

namespace IncidentFormValidationControl.Validation
{
    public class TextRule : ValidationRule
    {
        public TextRule()
        {
        }

        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            try
            {
                if (value == null || (string)value == "") return new ValidationResult(false, "");
            }
            catch
            {
                return new ValidationResult(false, "");
            }
            return new ValidationResult(true, null);
        }
    }
}

This defines a simple Validation rule. The variable “value” is checked, if it is null or empty, the rule fails. If it contains some text, the rule succeeds. The SCSM console form framework will automatically enable and disable the OK and Apply buttons based on failed Validation Rules.

Next, remove the default UserControl1 and add a new “UserControl (WPF)” called ValidationControl.xaml using “Right-click\Add\User Control” (this is easier than renaming the existing control):

Validation

Select your new control and hit SHIFT-F7 to open the XAML and replace it all with this:

<UserControl x:Class="IncidentFormValidationControl.ValidationControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             Visibility="Collapsed"
             Width="Auto" Height="Auto" DataContextChanged="FormControl_DataContextChanged" >
    <Grid Height="Auto" Width="Auto">
        <TextBox Height="0" Margin="0,0,0,0" Name="textId" VerticalAlignment="Top" Width="0" Text="{Binding Path=$Id$, Mode=OneWay}" Visibility="Collapsed" />
    </Grid>
</UserControl>

This defines a simple hidden control with a binding to the main form data context (IDataItem) only.

Now hit F7 to view the code for this control and replace it all with:

/*Custom control to add validation to the SCSM 012 Incident Form
 * 
 * Rob Ford 2014
 * 
 */

//Standard for .NET 3.5 WPF control
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

//Additional
using System.ComponentModel;
using System.ComponentModel.Design;

//SCSM
using Microsoft.EnterpriseManagement;
using Microsoft.EnterpriseManagement.Common;
using Microsoft.EnterpriseManagement.Configuration;
using Microsoft.EnterpriseManagement.UI.DataModel;
using Microsoft.EnterpriseManagement.UI.SdkDataAccess;
using Microsoft.EnterpriseManagement.ConsoleFramework;
using Microsoft.EnterpriseManagement.UI.WpfControls;
using Microsoft.EnterpriseManagement.GenericForm;

//Local
using IncidentFormValidationControl.Validation;

namespace IncidentFormValidationControl
{
    public partial class ValidationControl : UserControl
    {
        public ValidationControl()
        {
            InitializeComponent();
        }

        private void FormControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (this.DataContext != null && this.DataContext is IDataItem)
            {
                //Do not add validation if in template mode
                if (!FormUtilities.Instance.IsFormInTemplateMode(this)) ProcessControls();
            }
        }

        private void ProcessControls()
        {
            try
            {
                //Attempt to get the parent TabControl of our custom control
                //There is a TabControl near the top of the tree, we want the one below that
                DependencyObject doParentRoot = GetParentDependancyObject(this, "System.Windows.Controls.TabControl");

                //Did we get the TabControl?
                if (doParentRoot != null)
                {
                    //Now process each TabItem on the tab control
                    foreach (DependencyObject doChild in LogicalTreeHelper.GetChildren(doParentRoot))
                    {
                        //Is this a TabItem?
                        if (doChild.GetType().ToString() == "System.Windows.Controls.TabItem")
                        {
                            //Process each child on current tab to find ones we want and add validation rules
                            AddValidation(doChild);
                        }
                    }
                }
               
            }
            catch
            {
            }
        }

        //Navigates the Logical tree for passed parent and adds required validation
        private void AddValidation(DependencyObject rootparent)
        {
            //Navigate the logical tree for the children          
            foreach (var rootChild in LogicalTreeHelper.GetChildren(rootparent))
            {
                try
                {
                    if (rootChild is DependencyObject)
                    {

                        if (rootChild is TextBox && ((TextBox)rootChild).Name == "IncidentDescription")
                        {
                            //Add the rule so that this property is now required
                            TextBox tb = (TextBox)rootChild;
                            TextRule rule = new TextRule();
                            BindingOperations.GetBinding(tb, TextBox.TextProperty).ValidationRules.Add(rule);
                        }                       
                    }
                }
                catch
                {
                }

                //Process further logical children of this child
                if (rootChild is DependencyObject) AddValidation(rootChild as DependencyObject);
            }
        }

        //Returns specified parent object, if found, if name is empty, then navigate to top
        private DependencyObject GetParentDependancyObject(DependencyObject child, string name)
        {
            try
            {
                //We need the logical tree to get our parent
                DependencyObject parent = LogicalTreeHelper.GetParent(child);
                DependencyObject lastparent = null;

                //Is the parent our specified control?
                if (name != "" && parent.GetType().ToString() == name) return parent;

                //No, process further
                while (parent != null)
                {
                    string s = parent.GetType().ToString();
                    if (s == name && name != "") return parent;
                    parent = LogicalTreeHelper.GetParent(parent);
                    if (parent != null) lastparent = parent;
                }
                //Return results
                if (name != "") return null;
                else return lastparent;
            }
            catch
            {
                return null;
            }
        }
    }
}

This code checks to see if the form is in template mode when the data context changes (FormControl_DataContextChanged), if not, it adds the validation. It is very important to not add validation when in template mode, otherwise, you may not be able to save any templates.

The control navigates on the WPF visual and logical trees to find the parent Tab Control on the form, and then process each Tab Item (ProcessControls) to find controls of the required type (in this case, TextBox) and of the required name (in this case, “IncidentDescription”) and adds the validation rules (AddValidation).

Hit F6 to build your new DLL and copy it somewhere for the next step, creating a Management Pack to customise your Incident form. If you already have a customised Incident Form, jump to the step below that explains how to add this DLL to your Management Pack.

To make this step easier, I have included a complete Management Pack for you:

<ManagementPack ContentReadable="true" SchemaVersion="2.0" OriginalSchemaVersion="1.1" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <Manifest>
    <Identity>
      <ID>CustomIncidentForm</ID>
      <Version>1.0.0.0</Version>
    </Identity>
    <Name>CustomIncidentForm</Name>
    <References>
      <Reference Alias="System">
        <ID>System.Library</ID>
        <Version>7.5.8501.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="Console">
        <ID>Microsoft.EnterpriseManagement.ServiceManager.UI.Console</ID>
        <Version>7.5.2905.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="Alias_14edb292_d724_401a_b2a2_fb0fe0b79b77">
        <ID>ServiceManager.IncidentManagement.Library</ID>
        <Version>7.5.2905.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="Alias_b95f4b7a_d0ad_4151_a8d1_65504c4a8ae3">
        <ID>System.WorkItem.Incident.Library</ID>
        <Version>7.5.2905.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d">
        <ID>System.WorkItem.Library</ID>
        <Version>7.5.2905.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="Alias_f9a8dd13_131c_42d2_ad8a_679890dc833a">
        <ID>System.WorkItem.Activity.Library</ID>
        <Version>7.5.2905.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="Alias_7175ef0f_429a_4777_9a37_15e4fb0d7314">
        <ID>System.Knowledge.Library</ID>
        <Version>7.5.2905.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="Alias_d2a776f9_11cf_43f3_97e8_0fb064eca23a">
        <ID>System.SupportingItem.Library</ID>
        <Version>7.5.2905.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
    </References>
  </Manifest>
  <TypeDefinitions>
    <EntityTypes>
      <TypeProjections>
        <TypeProjection ID="CustomForm_307806ba_d764_4d35_b1b2_c527da8a6f69_TypeProjection" Accessibility="Public" Type="Alias_b95f4b7a_d0ad_4151_a8d1_65504c4a8ae3!System.WorkItem.Incident">
          <Component Path="$Context/Path[Relationship='Alias_b95f4b7a_d0ad_4151_a8d1_65504c4a8ae3!System.WorkItem.IncidentPrimaryOwner']$" Alias="PrimaryOwner" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemAffectedUser']$" Alias="AffectedUser" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemAssignedToUser']$" Alias="AssignedUser" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemCreatedByUser']$" Alias="CreatedByUser" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItem.TroubleTicketClosedByUser']$" Alias="ClosedByUser" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItem.TroubleTicketResolvedByUser']$" Alias="ResolvedByUser" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItem.TroubleTicketHasActionLog']$" Alias="ActionLogs" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItem.TroubleTicketHasUserComment']$" Alias="UserComments" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItem.TroubleTicketHasAnalystComment']$" Alias="AnalystComments" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItem.TroubleTicketHasNotificationLog' TypeConstraint='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItem.TroubleTicket.SmtpNotificationLog']$" Alias="SMTPNotifications" />
          <Component Path="$Context/Path[Relationship='Alias_f9a8dd13_131c_42d2_ad8a_679890dc833a!System.WorkItemContainsActivity']$" Alias="Activities">
            <Component Path="$Context/Path[Relationship='Alias_f9a8dd13_131c_42d2_ad8a_679890dc833a!System.WorkItemContainsActivity' SeedRole='Target']$" Alias="ParentWorkItem" />
            <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemCreatedByUser']$" Alias="ActivityCreatedBy" />
            <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemAssignedToUser']$" Alias="ActivityAssignedTo" />
            <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemAboutConfigItem']$" Alias="ActivityAboutConfigItem" />
          </Component>
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemRelatesToWorkItem']$" Alias="RelatedWorkItems">
            <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemAffectedUser']$" Alias="RWIAffectedUser" />
            <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemAssignedToUser']$" Alias="RWIAssignedUser" />
          </Component>
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemRelatesToWorkItem' SeedRole='Target']$" Alias="RelatedWorkItemsSource">
            <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemAffectedUser']$" Alias="RWIAffectedUser" />
            <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemAssignedToUser']$" Alias="RWIAssignedUser" />
          </Component>
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemAboutConfigItem']$" Alias="AffectedConfigItems" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemRelatesToConfigItem']$" Alias="RelatedConfigItems" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemAboutConfigItem' TypeConstraint='System!System.Service']$" Alias="RelatedServiceRequests" />
          <Component Path="$Context/Path[Relationship='Alias_7175ef0f_429a_4777_9a37_15e4fb0d7314!System.EntityLinksToKnowledgeDocument']$" Alias="RelatedKnowledgeArticles" />
          <Component Path="$Context/Path[Relationship='Alias_532d0597_93b6_4ffa_91dc_6f91a9413a4d!System.WorkItemHasFileAttachment']$" Alias="FileAttachments">
            <Component Path="$Context/Path[Relationship='Alias_d2a776f9_11cf_43f3_97e8_0fb064eca23a!System.FileAttachmentAddedByUser']$" Alias="FileAttachmentAddedBy" />
          </Component>
        </TypeProjection>
      </TypeProjections>
    </EntityTypes>
  </TypeDefinitions>
  <Categories>
    <Category ID="CustomIncidentForm.Category" Value="Console!Microsoft.EnterpriseManagement.ServiceManager.ManagementPack">
      <ManagementPackName>CustomIncidentForm</ManagementPackName>
      <ManagementPackVersion>1.0.0.0</ManagementPackVersion>
      <!--You must change this value to your own public key token-->
      <!--See http://scsmnz.net/sealing-a-management-pack-using-fastseal-exe/-->
      <ManagementPackPublicKeyToken>9387237119dbdd8f</ManagementPackPublicKeyToken>
    </Category>
  </Categories>
  <Presentation>
    <Forms>
      <Form ID="CustomForm_307806ba_d764_4d35_b1b2_c527da8a6f69" Accessibility="Public" Target="CustomForm_307806ba_d764_4d35_b1b2_c527da8a6f69_TypeProjection" BaseForm="Alias_14edb292_d724_401a_b2a2_fb0fe0b79b77!System.WorkItem.Incident.ConsoleForm" TypeName="Microsoft.EnterpriseManagement.ServiceManager.Incident.Forms.IncidentFormControl">
        <Category>Form</Category>
        <Customization>
          <AddControl Parent="UpperGeneralGrid" Assembly="IncidentFormValidationControl" Type="IncidentFormValidationControl.ValidationControl" Left="49.5" Top="458" Right="0" Bottom="0" Row="0" Column="0" />
        </Customization>
      </Form>
    </Forms>
  </Presentation>
  <LanguagePacks>
    <LanguagePack ID="ENU" IsDefault="true">
      <DisplayStrings>
        <DisplayString ElementID="CustomIncidentForm">
          <Name>CustomIncidentForm</Name>
        </DisplayString>
        <DisplayString ElementID="CustomForm_307806ba_d764_4d35_b1b2_c527da8a6f69">
          <Name>Customised Incident Form</Name>
          <Description>Adds additional validation to the Incident Form</Description>
        </DisplayString>
      </DisplayStrings>
    </LanguagePack>
  </LanguagePacks>
  <Resources>
    <Assembly ID="IncidentFormValidationControl.assembly" Accessibility="Public" QualifiedName="IncidentFormValidationControl" FileName="IncidentFormValidationControl.dll" />
  </Resources>
</ManagementPack>

If you already have a customised Incident Form, add this line to the end of your “Customization” block:

<AddControl Parent="UpperGeneralGrid" Assembly="IncidentFormValidationControl" Type="IncidentFormValidationControl.ValidationControl" Left="49.5" Top="458" Right="0" Bottom="0" Row="0" Column="0" />

And add the assembly resource for the DLL:

<Resources>
  <Assembly ID="IncidentFormValidationControl.assembly" Accessibility="Public" QualifiedName="IncidentFormValidationControl" FileName="IncidentFormValidationControl.dll" />
</Resources>

Note – how to determine where and how to add such a custom control is explained here.

You must now seal your Management Pack by following these instructions on using fastseal.exe.

You can seal using the Authoring Tool, but sometimes this tool will remove customisations it does not understand, so just be careful and check the results.

You will need to follow the instructions on sealing to change your public key token value for ManagementPackPublicKeyToken if using fastseal.exe. If you are using the AT, you must remove this value by removing this part of the Management Pack category (remove this entire line):

<ManagementPackPublicKeyToken>9387237119dbdd8f</ManagementPackPublicKeyToken>

Next, you need to bundle your Management Pack. To do this, follow the instructions part way down on this post.

Now, import your new Management Pack into Service Manager. Sometimes, on certain systems, you might have to restart the Management Server services to pick up MP changes. Restart your SCSM console and open an Incident and now you should see that the Description is required:

Description

Lastly, in order to find the name of the controls you wish to add validation to, use the Authoring Tool to examine the form, select the required control and obtain the control’s name:

Name

That’s it! Now you can use this example to add all the required properties you need.

For different types of control, you may require new Validation Rules, for example, to make a UserPicker required use:

public class UserPickerRule : ValidationRule
{
    public UserPickerRule()
    {
    }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        try
        {
            if (value == null) return new ValidationResult(false, null);
            return new ValidationResult(true, null);
        }
        catch
        {
            return new ValidationResult(false, null);
        }      
    }
}

And adjust the code in AddValidation() to look for your UserPicker:

else if (rootChild is UserPicker && ((UserPicker)rootChild).Name == "userpicker1")
{
    UserPicker up = (UserPicker)rootChild;
    UserPickerRule rule = new UserPickerRule();
    BindingOperations.GetBinding(up, UserPicker.UserProperty).ValidationRules.Add(rule);
}

This technique works on any form, you just need to change the control names to look for and the Parent for the AddControl customisation block.

Posted in Code, Management Packs | Tagged , , , | 13 Comments