How-To: Создание собственного UserControl для SCSM 2010 SP1
01.07.2011 2 комментария
ВНИМАНИЕ! Описанные здесь техники в основном являются не документированными и не поддерживаемыми со стороны 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 Files\Microsoft System Center\Service Manager 2010\SDK Binaries на сервере SCSM)
- Microsoft.EnterpriseManagement.UI.Foundation.dll (расположена в папке c:\Program Files\Microsoft System Center\Service Manager 2010\)
- Microsoft.EnterpriseManagement.UI.SdkDataAccess.dll (расположена в папке c:\Program Files\Microsoft System Center\Service Manager 2010\)
Если вы планируете использовать стандартные контролы SCSM вам также понадобиться подключить библиотеки Microsoft.EnterpriseManagement.UI.Controls.dll, Microsoft.EnterpriseManagement.UI.ExtendedControls.dll, Microsoft.EnterpriseManagement.UI.SMControls.dll и WPFToolKit.dll
Все контролы, которые мы подключаем через расширение форм, обязаны иметь атрибут ContentProperty. С чем именно связано такое требование, мне выяснить не удалось, но такое требование обязательно. В связи с этим нам необходимо создать переменную, которую мы будем использовать в качестве значения для атрибута ContentProperty. Тип и название переменной могут быть любыми, но она должна быть определена с помощью
DependencyProperty.
В итоге у вас должно получиться нечто, похожее на это:
010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445namespace
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). Несколько примеров:
1234IDataItem 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.Чтобы использовать эти значения в привязке, достаточно указать название свойства. Добавим на наш контрол несколько элементов:
12345678<
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):
1234567891011121314151617181920212223[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, и когда там оказывается нужный нам объект – устанавливаем свойства:
12345678910111213141516private
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 SDKs\Windows\v7.0A\Bin\). Можно добавить эту команду в инструменты Visual Studio
- При необходимости, добавить другие свойства с помощью элемента <PropertyChange>
Вот пример для формы инцидента:
12345678910111213141516171819202122232425262728293031<
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%\AppData\Local\Microsoft\System Center Service Manager 2010\%GROUPNAME%\%MPVERSION%,
где
%GROUPNAME% – имя группы управления
%MPVERSION% – версия пакета управления, который содержит ссылку на ресурс
Нам достаточно добавить в конце нашего пакет управления ссылку на сборку:
123456
</
LanguagePacks
>
<!-- Section For Assembly -->
<
Resources
>
<
Assembly
ID
=
"SCSMControlAssembly"
Accessibility
=
"Public"
QualifiedName
=
"SCSMControl"
FileName
=
"SCSMControl.dll"
/>
</
Resources
>
</
ManagementPack
>
а затем упаковать его в бандл. Для упаковки вы можете использовать скрипт по ссылке выше или мою утилиту MPBMaker (перед упаковкой не забудьте скопировать библиотеку в ту же папку, где расположен пакет управления):
MPBMaker.exe SCSMControlBundle “d:\Examples\Example.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, а также готовый пакет управления вы можете здесь.
Очень познавательная статья. Особенно порадовала возможность делать обязательными другие поля на форме, например «Описание», бывает полезным. Есть один вопросик — после кастомизации МП можно ли его запечатать, потому что незапечатав его, не получится применять к расширенной форме шаблоны.
Конечно можно.