ВНИМАНИЕ! Описанные здесь техники в основном являются не документированными и не поддерживаемыми со стороны Microsoft. Информация предоставлена “как есть”, автор не несет ответственности за возможную потерю инфомрацию.
Вступление
Всё чаще я встречаю вопросы о том, как можно расширить функциональность SCSM с помощью собственных форм или контролов. Лично я предпочитаю не использовать полностью переписанные формы без крайней необходимости: это довольно большой объем работы, и при выходе новой версии продукта (выход намечен на Q4 2011 – Q1 2012) с почти 100% гарантией ваша форма перестанет работать. Контролы требуют меньше времени на разработку, а шанс, что они будут работать и в следующей версии продукта, довольно велик.
Чтобы понимать, о чем пойдет речь в данной статье, необходимо иметь представление о следующих технологиях:
- WPF (Windows Presentation Foundation), в особенности Binding
- DependencyProperty
- XML-схема пакета управления
Итак, нам необходимо решить следующие задачи:
- Создать новый UserControl в Visual Studio
- Подключить этот контрол к форме
- Доставить библиотеку с контролом на все компьютеры с консолью SCSM
Создание UserControl-а
Для создания нового UserControl-а необходимо открыть Visual Studio, и выбрать новый проект WPF User Conrtol Library:
Имя решения и имя класса могут быть любыми. После этого необходимо отредактировать название контрола.
Затем необходимо подключить к проекту несколько библиотек:
- Microsoft.EnterpriseManagement.Core.dll (расположена в папке c:Program FilesMicrosoft System CenterService Manager 2010SDK Binaries на сервере SCSM)
- Microsoft.EnterpriseManagement.UI.Foundation.dll (расположена в папке c:Program FilesMicrosoft System CenterService Manager 2010)
- Microsoft.EnterpriseManagement.UI.SdkDataAccess.dll (расположена в папке c:Program FilesMicrosoft System CenterService Manager 2010)
Если вы планируете использовать стандартные контролы SCSM вам также понадобиться подключить библиотеки Microsoft.EnterpriseManagement.UI.Controls.dll, Microsoft.EnterpriseManagement.UI.ExtendedControls.dll, Microsoft.EnterpriseManagement.UI.SMControls.dll и WPFToolKit.dll
Все контролы, которые мы подключаем через расширение форм, обязаны иметь атрибут ContentProperty. С чем именно связано такое требование, мне выяснить не удалось, но такое требование обязательно. В связи с этим нам необходимо создать переменную, которую мы будем использовать в качестве значения для атрибута ContentProperty. Тип и название переменной могут быть любыми, но она должна быть определена с помощью
DependencyProperty.
В итоге у вас должно получиться нечто, похожее на это:
namespace SCSMControls { [ContentProperty("SelectedItem")] public partial class SCSMControl : UserControl, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public SCSMControl() { InitializeComponent(); } public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem", typeof(string), typeof(SCSMControl), new UIPropertyMetadata(null, new PropertyChangedCallback(SCSMControl.OnSelectedItemChanged))); public string SelectedItem { get { return (string)base.GetValue(SelectedItemProperty); } set { base.SetValue(SelectedItemProperty, value); NotifyPropertyChanged("SelectedItem"); } } private static void OnSelectedItemChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { //TODO } /// <summary> /// INotifyPropertyChanged implementation /// </summary> /// <param name="propertyName"></param> private void NotifyPropertyChanged(string propertyName) { if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } }
Я также добавил интерфейс INotifyPropertyChanged, чтобы внешние компоненты могли подписываться на изменение свойств нашего контрола. Кроме этого, я показал как можно подписаться на изменение свойства нашего компонента. Иногда это бывает полезно. Но использовать оба эти подхода не обязательно.
Теперь наш контрол можно добавлять на форму в SCSM, но он пока не делает ничего полезного. Для начала нужно отобразить какие-нибудь данные. В этом нам поможет такой мощный механизм WPF, как привязка (binding). Но чтобы использовать привязку, необходимо знать к каким свойствам привязываться.
DataContext формы (а значит и нашего контрола) заполняется объектом типа IDataItem. Этот тип не документирован, поэтому вам придется исследовать его самим или поверить мне)). Объект типа IDataItem хранит в себе все свойства объекта, для которого открыта форма. Доступ к объектам осуществляется операцией взятия индекса ([]), а индексатором выступает внутреннее имя поля (или имя Type Projection). Несколько примеров:
IDataItem item = this.DataContext as IDataItem; string title = (string)item["Title"]; string status = (string)(item["Status"] as IDataItem)["DisplayName"]; string affectedUser = (string)(item["AffectedUser"] as IDataItem)["DisplayName"];
Как видно из примера, мы можем обращаться к вложенным свойствам полученных объектов также через IDataItem.Чтобы использовать эти значения в привязке, достаточно указать название свойства. Добавим на наш контрол несколько элементов:
<TextBox Name="boxID" Grid.Column="1" Grid.Row="0" Text="{Binding Path=$Id$, Mode=OneWay}" VerticalAlignment="Center" HorizontalAlignment="Stretch" IsReadOnly="True" /> <TextBox Name="boxName" Grid.Column="1" Grid.Row="1" Text="{Binding Path=Id, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" HorizontalAlignment="Stretch" /> <TextBox Name="boxAffectedUser" Grid.Column="1" Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Stretch" IsReadOnly="True" > <TextBox.Text> <Binding Path="AffectedUser.DisplayName" Mode="OneWay" FallbackValue="No Affected User"/> </TextBox.Text> </TextBox> <TextBox Name="boxItem" Grid.Column="1" Grid.Row="3" Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=SelectedItem}" VerticalAlignment="Center" HorizontalAlignment="Stretch" />
Здесь показаны несколько приемов привязки. В первом случае мы привязываемся к внутреннему идентификатору элемента (тип Guid). Во втором случае – к стандартному. Третий способ показывает, как можно задать текст для случая, если значение поля пустое. Заметьте, что это не значение по-умолчанию, а именно подстановка текста. Четвертый способ показывает, как можно сделать привязку к свойствам нашего контрола.
Итак, наш контрол умеет отображать данные. Но не плохо бы научить его также и изменять данные. IDataItem не слишком хорошо подходит для манипуляции с объектами SDK, т.к. он содержит лишь информацию об одном объекте. Не плохо бы получить доступ к SDK, т.к. с помощью него мы можем производить любые манупуляции с данными. Это мы можем сделать следующим образом (см. функцию GetSession):
[ContentProperty("SelectedItem")] public partial class SCSMControl : UserControl, INotifyPropertyChanged { EnterpriseManagementGroup mg; public event PropertyChangedEventHandler PropertyChanged; public SCSMControl() { InitializeComponent(); GetSession(); } void GetSession() { // Get the current session, more info: http://blogs.technet.com/servicemanager/archive/2010/02/11/tasks-part-1-tasks-overview.aspx IServiceContainer container = (IServiceContainer)FrameworkServices.GetService(typeof(IServiceContainer)); IManagementGroupSession curSession = (IManagementGroupSession)container.GetService(typeof(IManagementGroupSession)); if (curSession == null) throw new ValueUnavailableException("curSession is null"); mg = curSession.ManagementGroup; } }
Итак, мы имеет доступ к SDK, давайте сделаем что-нибудь полезное. Н-р установим свойства по-умолчанию для нового объекта. Для этого нам необходимо получить объект IDataItem из DataContext, а затем установить свойства. Делать это в обработчике события FormLoaded не стоит – в этом момент DataContext еще не заполнен. Вместо этого мы подписываемся на изменение свойства DataContext, и когда там оказывается нужный нам объект – устанавливаем свойства:
private void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { // wait binding if (this.DataContext is IDataItem) { instance = (this.DataContext as IDataItem); // If this is new incident, set some default properties if ((bool)instance["$IsNew$"]) { instance["Title"] = "WOW! Now we can set the default value for property!!!"; instance["Description"] = "And we can use SDK. Current management group: " + mg.Name; //IncidentTierQueuesEnum.Tier2 instance["TierQueue"] = mg.EntityTypes.GetEnumeration(new Guid("df3896f5-3145-0546-4d25-e485de6765af")); } } }
Обратите внимание на свойство $IsNew$ – оно определяет открыта ли форма для создание элемента (true) или для редактирования (false).
Итак, на этом наш компонент полностью готов. Проект Vusial Studio 2010 с примером вы можете скачать в конце статьи.
Добавление контрола на форму
Теперь нам необходимо добавить наш контрол на форму. Для этого нам потребуется создать с помощью Authoring Tool новую модификацию для формы, а затем отредактировать её XML-код в любом редакторе. Чтобы добавить собственный контрол на форму необходимо:
- Создать новую модификацию формы. Как это сделать описано много где в сети, например здесь и здесь, а вот здесь даже с видео
- Добавить на форму в место, где должен находится наш конрол, любой стандартный контрол, например Label
- Сохранить пакет управления, затем открыть его в любом текстовом редакторе.
- Найти секцию Forms, а в ней добавленный контрол.
- Заменить атрибуты Assembly и Type на данные нашего контрола. PublicKey можно узнать с помощью каманды sn.exe –T <путь к сборке> (находится в папке c:Program Files (x86)Microsoft SDKsWindowsv7.0ABin). Можно добавить эту команду в инструменты Visual Studio
- При необходимости, добавить другие свойства с помощью элемента <PropertyChange>
Вот пример для формы инцидента:
<Form ID="CustomForm_650994d4_1a69_4f1a_975f_7d060b90a3f3" Accessibility="Public" Target="CustomForm_650994d4_1a69_4f1a_975f_7d060b90a3f3_TypeProjection" BaseForm="Alias_cf5e40e8_e299_4731_9572_41860eb78176!System.WorkItem.Incident.ConsoleForm" TypeName="Microsoft.EnterpriseManagement.ServiceManager.Incident.Forms.IncidentFormControl"> <Category>Form</Category> <Customization> <AddControl Parent="StackPanel206" Assembly="PresentationFramework, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Type="System.Windows.Controls.Label" Left="92" Top="14.2" Right="0" Bottom="0" Row="0" Column="0" /> <PropertyChange Object="Label_1" Property="HorizontalAlignment"> <NewValue>Left</NewValue> </PropertyChange> <PropertyChange Object="Label_1" Property="VerticalAlignment"> <NewValue>Bottom</NewValue> </PropertyChange> <PropertyChange Object="Label_1" Property="Content"> <NewValue>Custom control:</NewValue> </PropertyChange> <AddControl Parent="StackPanel206" Assembly="SCSMControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e2bef97f2bc99659" Type="SCSMControls.SCSMControl" Left="100.8" Top="9.40000000000003" Right="46" Bottom="0" Row="0" Column="0" /> <PropertyChange Object="SCSMControl_1" Property="Width"> <NewValue>Auto</NewValue> </PropertyChange> <PropertyChange Object="SCSMControl_1" Property="Height"> <NewValue>Auto</NewValue> </PropertyChange> <PropertyChange Object="SCSMControl_1" Property="VerticalAlignment"> <NewValue>Bottom</NewValue> </PropertyChange> <PropertyChange Object="SCSMControl_1" Property="Margin"> <NewValue>0,0,0,0</NewValue> </PropertyChange> <PropertyChange Object="SCSMControl_1" Property="SelectedItem"> <NewValue>Cool control</NewValue> </PropertyChange> </Customization> </Form>
В итоге у меня получился вот такой контрол:
Для новых инцидентов автоматически заполняется Название и Группа подержки:
Доставка библиотеки
Теперь нам необходимо скопировать нашу библиотеку на все рабочие станции с консолью SCSM. Хорошо, если их 3-4, а если их 50?100? А как потом обновлять эту библиотеку? Не самые приятные и простые вопросы.
К счастью для нас, разработчики SCSM позаботились об этом. В SCSM существует так называемый бандл пакетов управления (management pack bundle). Данный тип пакетов управления может содержать в себе другие пакеты управления (как запечатанные, так и нет), а также различные сборки, изображения и прочие ресурсы.
Необходимо лишь перед созданием бандла в пакете управления указать ссылку на ресурс (в нашем случае библиотека). Добавленные таки образом ресурсы копируются в локальный профиль пользователя, который запускает консоль, в папку %USERPROFILE%AppDataLocalMicrosoftSystem Center Service Manager 2010%GROUPNAME%%MPVERSION%,
где
%GROUPNAME% – имя группы управления
%MPVERSION% – версия пакета управления, который содержит ссылку на ресурс
Нам достаточно добавить в конце нашего пакет управления ссылку на сборку:
</LanguagePacks> <!-- Section For Assembly --> <Resources> <Assembly ID="SCSMControlAssembly" Accessibility="Public" QualifiedName="SCSMControl" FileName="SCSMControl.dll" /> </Resources> </ManagementPack>
а затем упаковать его в бандл. Для упаковки вы можете использовать скрипт по ссылке выше или мою утилиту MPBMaker (перед упаковкой не забудьте скопировать библиотеку в ту же папку, где расположен пакет управления):
MPBMaker.exe SCSMControlBundle “d:ExamplesExample.SCSMControl.xml”
Полученный пакет необходимо импортировать в SCSM.
Возникающие ошибки
Если во время импортирования пакета управления вы получили ошибку вроде этой:
: Failed to verify form: CustomForm_650994d4_1a69_4f1a_975f_7d060b90a3f3
The form base is not valid. Form CustomForm_650994d4_1a69_4f1a_975f_7d060b90a3f3 extends form System.WorkItem.Incident.ConsoleForm, which already has another extension (CustomForm_8c4ec25b_1dc3_4b58_bd43_7c8a83f619a0)
то это означает, что форма уже модифицированна в другом пакете управления.
Если после импортирования пакета ваш контрол выглядит вот таким образом:
то скорее всего вы забыли добавить атрибут ContentProperty.
Заключение
С помощью собственных контролов вы можете полностью контролировать поведение формы:
- Задавать значение по-умолчанию для свойств
- Изменять параметры других контролов (н-р отключать или включать обязательность полей)
- Отключать или прятать другие контролы на основе каких-то параметров или роли пользователя
Скачать готовый проект с примером на Visual Studio, а также готовый пакет управления вы можете здесь.
Возможно ли написание своей формы, альтернативной существующей. Например, для инцидентов, чтобы класс использовался тот же самый (чтобы не потерять весь функционал, завязанный на этом классе), а форма ввода/редактирования была своя с необходимым функционалом.
Или такой трюк в ServiceManager в принципе невозможен?
Можно. Создайте свой контрол и укажите как цель тот же type projection.