Compare commits

...

7 Commits

17 changed files with 841 additions and 11 deletions

View File

@@ -17,15 +17,27 @@ namespace PepperDash.Essentials.Core.Config
[JsonProperty("info")]
public InfoConfig Info { get; set; }
/// <summary>
/// Defines the devices in the system
/// </summary>
[JsonProperty("devices")]
public List<DeviceConfig> Devices { get; set; }
/// <summary>
/// Defines the source list for the system
/// </summary>
[JsonProperty("sourceLists")]
public Dictionary<string, Dictionary<string, SourceListItem>> SourceLists { get; set; }
/// <summary>
/// Defines all the tie lines for system routing
/// </summary>
[JsonProperty("tieLines")]
public List<TieLineConfig> TieLines { get; set; }
/// <summary>
/// Defines any join maps to override the default join maps
/// </summary>
[JsonProperty("joinMaps")]
public Dictionary<string, string> JoinMaps { get; set; }

View File

@@ -11,24 +11,43 @@ using PepperDash.Essentials.Core;
namespace PepperDash.Essentials.Core.Config
{
public class DeviceConfig
public class DeviceConfig : PropertiesConfigBase
{
[JsonProperty("key")]
/// <summary>
/// The unique idendifier for the device
/// </summary>
[JsonProperty("key", Required = Required.Always)]
public string Key { get; set; }
/// <summary>
/// A unique ID for each device instance. Used to differentiate between devices of the same type that may have
/// been added/removed from the system
/// </summary>
[JsonProperty("uid")]
public int Uid { get; set; }
/// <summary>
/// The name of the device
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// The group for the device
/// </summary>
[JsonProperty("group")]
public string Group { get; set; }
[JsonProperty("type")]
/// <summary>
/// The type of the device to instantiate
/// </summary>
[JsonProperty("type", Required = Required.Always)]
public string Type { get; set; }
[JsonProperty("properties")]
/// <summary>
/// The properties necessary to define the device
/// </summary>
[JsonProperty("properties", Required = Required.Always)]
[JsonConverter(typeof(DevicePropertiesConverter))]
public JToken Properties { get; set; }
}
@@ -59,7 +78,7 @@ namespace PepperDash.Essentials.Core.Config
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException("SOD OFF HOSER");
throw new NotImplementedException("Not Supported");
}
}
}

View File

@@ -4,6 +4,7 @@ using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using PepperDash.Core;
using PepperDash.Core.Config;
@@ -91,9 +92,24 @@ namespace PepperDash.Essentials.Core.Config
{
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Loading config file: '{0}'", filePath);
var directoryPrefix = string.Format("{0}Config{1}Schema{1}", Global.ApplicationDirectoryPrefix, Global.DirectorySeparator);
var schemaFilePath = directoryPrefix + "EssentialsConfigSchema.json";
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Loading Schema from path: {0}", schemaFilePath);
var jsonConfig = fs.ReadToEnd();
if(File.Exists(schemaFilePath))
{
// Attempt to validate config against schema
ValidateSchema(jsonConfig, schemaFilePath);
}
else
Debug.Console(0, Debug.ErrorLogLevel.Warning, "No Schema found at path: {0}", schemaFilePath);
if (localConfigFound)
{
ConfigObject = JObject.Parse(fs.ReadToEnd()).ToObject<EssentialsConfig>();
ConfigObject = JObject.Parse(jsonConfig).ToObject<EssentialsConfig>();
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Successfully Loaded Local Config");
@@ -101,7 +117,7 @@ namespace PepperDash.Essentials.Core.Config
}
else
{
var doubleObj = JObject.Parse(fs.ReadToEnd());
var doubleObj = JObject.Parse(jsonConfig);
ConfigObject = PortalConfigReader.MergeConfigs(doubleObj).ToObject<EssentialsConfig>();
// Extract SystemUrl and TemplateUrl into final config output
@@ -129,6 +145,40 @@ namespace PepperDash.Essentials.Core.Config
}
}
/// <summary>
/// Attempts to validate the JSON against the specified schema
/// </summary>
/// <param name="json">JSON to be validated</param>
/// <param name="schemaFileName">File name of schema to validate against</param>
public static void ValidateSchema(string json, string schemaFileName)
{
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Validating Config File against Schema...");
JObject config = JObject.Parse(json);
using (StreamReader fileStream = new StreamReader(schemaFileName))
{
JsonSchema schema = JsonSchema.Parse(fileStream.ReadToEnd());
if (config.IsValid(schema))
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Configuration successfully Validated Against Schema");
else
{
Debug.Console(0, Debug.ErrorLogLevel.Warning, "Validation Errors Found in Configuration:");
config.Validate(schema, Json_ValidationEventHandler);
}
}
}
/// <summary>
/// Event Handler callback for JSON validation
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
public static void Json_ValidationEventHandler(object sender, ValidationEventArgs args)
{
Debug.Console(0, "JSON Validation error at line {0} position {1}: {2}", args.Exception.LineNumber, args.Exception.LinePosition, args.Message);
}
/// <summary>
/// Returns all the files from the directory specified.
/// </summary>

View File

@@ -0,0 +1,181 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Control Properties",
"$ref": "#/definitions/ControlPropertiesConfig",
"definitions": {
"ControlPropertiesConfig": {
"description": "The method of communicating with the device",
"properties": {
"method": {
"type": "string",
"title": "Communication Method",
"enum": [
"none",
"com",
"ipid",
"ipidtcp",
"ir",
"ssh",
"tcpip",
"telnet",
"cresnet",
"cec",
"udp"
]
},
"tcpSshProperties": {
"$ref":"TcpSshPropertiesConfigSchema.json#definitions/TcpSshPropertiesConfig",
"title": "Properties for IP based communication",
"default": null
},
"comParams": {
"title":"Com Port parameters",
"description": "The parameters to configure the COM port",
"type":"object",
"protocol":{
"title":"Protocol",
"type":"string",
"enum":[
"RS232",
"RS422",
"RS485"
]
},
"baudRate":{
"title":"Baud Rate",
"type":"integer",
"enum":[
300,
600,
1200,
1800,
2400,
3600,
4800,
7200,
9600,
14400,
19200,
28800,
38400,
57600,
115200
]
},
"dataBits":{
"title":"Data Bits",
"type":"integer",
"enum":[
7,
8
]
},
"stopBits":{
"title":"Stop Bits",
"type":"integer",
"enum":[
1,
2
]
},
"parity":{
"title":"Parity",
"type":"string",
"enum":[
"None",
"Even",
"One"
]
},
"softwareHandshake":{
"title":"Software Handshake",
"type":"string",
"enum":[
"None",
"RTS",
"CTS",
"RTSCTS"
]
},
"hardwareHandshake":{
"title":"Hardware Handshake",
"type":"string",
"enum":[
"None",
"XON",
"XONT",
"XONR"
]
},
"pacing":{
"title":"Pacing",
"type":"integer"
}
},
"cresnetId":{
"type": "string",
"title":"Cresnet ID",
"description": "Cresnet ID of the device",
"default": "",
"examples": [
"13",
"03",
"0A",
"F1"
],
"pattern": "^(?!00|01|02|FF)[0-9,A-F,a-f][0-9,A-F,a-f]$"
},
"controlPortDevKey": {
"type": "string",
"title":"Port Device",
"description": "Key of the device where the control port is found",
"examples": [
"processor"
]
},
"controlPortNumber": {
"type": "integer",
"title": "Port Number",
"description": "Control Port Number on the device referenced by controlPortDevKey",
"examples": [
1
]
},
"controlPortName": {
"type": "string",
"title": "Port Name",
"description": "Control Port Name on the device referenced by controlPortDevKey",
"examples": [
"hdmi1"
]
},
"irFile": {
"type": "string",
"title": "IR File",
"description": "IR Filename",
"default": "",
"examples": [
"Comcast Motorola DVR.ir"
],
"pattern": "^(.*).ir$"
},
"ipid": {
"type": "string",
"title": "IPID",
"default": "",
"examples": [
"13",
"03",
"0A",
"F1"
],
"pattern": "^(?!00|01|02|FF)[0-9,A-F,a-f][0-9,A-F,a-f]$"
}
},
"required": [
"method"
]
}
}
}

View File

@@ -0,0 +1,282 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Essentials Configuration File Schema",
"description": "",
"type": "object",
"$ref":"#/definitions/EssentialsConfig",
"definitions": {
"EssentialsConfig": {
"type": "object",
"additionalProperties": false,
"properties": {
"template": {
"$ref": "#/definitions/BasicConfig"
},
"system_url": {
"type": "string",
"format": "uri",
"qt-uri-protocols": [
"https"
]
},
"system": {
"$ref": "#/definitions/BasicConfig"
},
"template_url": {
"type": "string",
"format": "uri",
"qt-uri-protocols": [
"https"
]
}
},
"required": [
"system",
"template"
],
"title": "Essentials Configuration"
},
"BasicConfig": {
"type": "object",
"additionalProperties": false,
"properties": {
"devices": {
"type": "array",
"items": {
"$ref": "#/definitions/Device"
}
},
"rooms": {
"type": "array",
"items": {
"$ref": "#/definitions/Room"
}
},
"info": {
"$ref": "#/definitions/Info"
},
"sourceLists": {
"$ref": "#/definitions/SourceLists"
}
},
"required": [
],
"title": "Basic Config"
},
"Info": {
"type": "object",
"additionalProperties": true,
"properties": {
"name":{
"type":"string"
},
"date":{
"type":"string",
"format": "date-time"
},
"version":{
"type":"string"
},
"runtimeInfo":{
"$ref":"#/definitions/RuntimeInfo"
},
"comment":{
"type":"string"
},
"hostname":{
"type":"string"
},
"appNumber":{
"type":"integer"
},
"lastModifiedDate": {
"type": "string",
"format": "date-time"
},
"lastUid":{
"type":"integer"
},
"processorType":{
"type":"string"
},
"systemType":{
"type":"string"
},
"requiredControlSoftwareVersion":{
"type":"string"
}
},
"required": [
],
"title": "Info"
},
"RuntimeInfo":{
"type":"object",
"additionalProperties": false,
"properties": {
"appName":{
"type":"string"
},
"assemblyVersion": {
"type":"string"
},
"osVersion":{
"type":"string"
}
}
},
"Device": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string"
},
"group": {
"type": "string"
},
"properties": {
"type":"object"
},
"uid": {
"type": "integer"
},
"key": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"group",
"key",
"properties",
"type"
],
"title": "Device"
},
"Room": {
"type": "object",
"additionalProperties": false,
"properties": {
"key": {
"type": "string"
},
"name": {
"type": "string"
},
"properties": {
"type": "object"
},
"type": {
"type": "string"
}
},
"required": [
"key",
"name",
"properties",
"type"
],
"title": "Room"
},
"SourceLists": {
"type": "object",
"additionalProperties": true,
"properties": {
"default":{
"$ref":"#/definitions/SourceList"
}
},
"title": "SourceLists"
},
"SourceList": {
"type": "object",
"additionalProperties": true,
"properties": {
},
"title": "Source List"
},
"Source": {
"type": "object",
"additionalProperties": false,
"properties": {
"icon": {
"type": "string"
},
"volumeControlKey": {
"type": "string"
},
"sourceKey": {
"type": "string"
},
"routeList": {
"type": "array",
"items": {
"$ref": "#/definitions/RouteList"
}
},
"includeInSourceList": {
"type": "boolean"
},
"type": {
"type": "string",
"enum":[
"route",
"off",
"other"
]
},
"altIcon": {
"type": "string"
},
"order": {
"type": "integer"
},
"disableCodecSharing": {
"type": "boolean"
}
},
"required": [
"includeInSourceList",
"order",
"routeList",
"sourceKey",
"type",
"volumeControlKey"
],
"title": "Source"
},
"RouteList": {
"type": "object",
"additionalProperties": false,
"properties": {
"destinationKey": {
"type": "string"
},
"sourceKey": {
"type": "string"
},
"type": {
"type": "string",
"enum":[
"audio",
"video",
"audioVideo",
"usbOutput",
"usbInput"
]
}
},
"required": [
"destinationKey",
"sourceKey",
"type"
],
"title": "RouteList"
}
}
}

View File

@@ -0,0 +1,81 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "TcpSshPropertiesConfig",
"$ref": "#/definitions/TcpSshPropertiesConfig",
"definitions": {
"TcpSshPropertiesConfig": {
"properties": {
"username": {
"type": "string",
"title": "Username",
"default":"",
"examples": [
"admin"
],
"pattern": "^(.*)$"
},
"port": {
"type": "integer",
"title": "Port Number",
"minimum": 1,
"maximum": 65535,
"examples": [
22,
23,
1515
]
},
"address": {
"type": "string",
"title": "IP Address or Hostname",
"examples": [
"192.168.99.100",
"myDeviceHostname"
],
"pattern": "^(.*)$"
},
"password": {
"type": "string",
"title": "Password",
"default":"",
"examples": [
"password"
],
"pattern": "^(.*)$"
},
"autoReconnect": {
"type": "boolean",
"title": "Auto Reconnect",
"description": "Indicates if automatic attemtps to reconnect should be made if communication is lost with the device",
"default": true,
"examples": [
true
]
},
"autoReconnectIntervalMs": {
"type": "integer",
"title": "Auto Reconnect Interval (Milliseconds)",
"description": "If Auto Reconnect is enabled, how often should reconnect attempts be made",
"default": 5000,
"examples": [
2000
]
},
"bufferSize": {
"type": "integer",
"title": "Buffer Size",
"description": "The size of the receive buffer to use",
"default": 32768,
"examples": [
32768
]
}
},
"required": [
"port",
"address"
]
}
}
}

View File

@@ -10,7 +10,9 @@ using PepperDash.Essentials.Core.Config;
namespace PepperDash.Essentials.Core.Devices
{
/// <summary>
///
/// This class should be inherited from when the configuration can be modified at runtime from another source other than the configuration file.
/// It contains the necessary properties, methods and events to allot the initial device configuration to be overridden and then notifies the
/// ConfigWriter to write out the new values to a local file to be read on next boot.
/// </summary>
public abstract class ReconfigurableDevice : Device
{

View File

@@ -67,34 +67,64 @@ namespace PepperDash.Essentials.Core
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// The icon to display
/// </summary>
[JsonProperty("icon")]
public string Icon { get; set; }
/// <summary>
/// Alternate icon to display
/// </summary>
[JsonProperty("altIcon")]
public string AltIcon { get; set; }
/// <summary>
/// Indicates if the source should be included in the list
/// </summary>
[JsonProperty("includeInSourceList")]
public bool IncludeInSourceList { get; set; }
/// <summary>
/// Determines the order the source appears in the list (ascending)
/// </summary>
[JsonProperty("order")]
public int Order { get; set; }
/// <summary>
/// Key of the volume control device for the source
/// </summary>
[JsonProperty("volumeControlKey")]
public string VolumeControlKey { get; set; }
/// <summary>
/// The type of source list item
/// </summary>
[JsonProperty("type")]
[JsonConverter(typeof(StringEnumConverter))]
public eSourceListItemType Type { get; set; }
/// <summary>
/// The list of routes to run when source is selected
/// </summary>
[JsonProperty("routeList")]
public List<SourceRouteListItem> RouteList { get; set; }
/// <summary>
/// Indicates if this source should be disabled for sharing via codec content
/// </summary>
[JsonProperty("disableCodecSharing")]
public bool DisableCodecSharing { get; set; }
/// <summary>
/// Indicates if this source should be disabled for local routing
/// </summary>
[JsonProperty("disableRoutedSharing")]
public bool DisableRoutedSharing { get; set; }
/// <summary>
/// The list of valid destination types for this source
/// </summary>
[JsonProperty("destinations")]
public List<eSourceListItemDestinationTypes> Destinations { get; set; }

View File

@@ -9,8 +9,14 @@ using PepperDash.Essentials.License;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Global application properties
/// </summary>
public static class Global
{
/// <summary>
/// The control system the application is running on
/// </summary>
public static CrestronControlSystem ControlSystem { get; set; }
public static LicenseManager LicenseManager { get; set; }
@@ -31,6 +37,19 @@ namespace PepperDash.Essentials.Core
}
}
/// <summary>
/// The file path prefix to the folder containing the application files (including embedded resources)
/// </summary>
public static string ApplicationDirectoryPrefix
{
get
{
string fmt = "00.##";
var appNumber = InitialParametersClass.ApplicationNumber.ToString(fmt);
return string.Format("{0}{1}Simpl{1}app{2}{1}", Crestron.SimplSharp.CrestronIO.Directory.GetApplicationRootDirectory(), Global.DirectorySeparator,appNumber );
}
}
/// <summary>
/// Wildcarded config file name for global reference
/// </summary>

View File

@@ -267,6 +267,15 @@
<Compile Include="SmartObjects\SubpageReferencList\SubpageReferenceList.cs" />
<Compile Include="SmartObjects\SubpageReferencList\SubpageReferenceListItem.cs" />
<None Include="app.config" />
<EmbeddedResource Include="Config\Schema\EssentialsConfigSchema.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Config\Schema\ControlPropertiesConfigSchema.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Config\Schema\TcpSshPropertiesConfigSchema.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Include="Properties\ControlSystem.cfg" />
</ItemGroup>
<ItemGroup>

View File

@@ -17,21 +17,39 @@ namespace PepperDash.Essentials.DM.Config
[JsonProperty("control")]
public ControlPropertiesConfig Control { get; set; }
/// <summary>
/// The available volume controls
/// </summary>
[JsonProperty("volumeControls")]
public Dictionary<uint, DmCardAudioPropertiesConfig> VolumeControls { get; set; }
/// <summary>
/// The input cards
/// </summary>
[JsonProperty("inputSlots")]
public Dictionary<uint, string> InputSlots { get; set; }
/// <summary>
/// The output cards (each card represents a pair of outputs)
/// </summary>
[JsonProperty("outputSlots")]
public Dictionary<uint, string> OutputSlots { get; set; }
/// <summary>
/// The names of the Inputs
/// </summary>
[JsonProperty("inputNames")]
public Dictionary<uint, string> InputNames { get; set; }
/// <summary>
/// The names of the Outputs
/// </summary>
[JsonProperty("outputNames")]
public Dictionary<uint, string> OutputNames { get; set; }
/// <summary>
/// The string to use when no route is set for a given output
/// </summary>
[JsonProperty("noRouteText")]
public string NoRouteText { get; set; }
@@ -49,9 +67,15 @@ namespace PepperDash.Essentials.DM.Config
/// </summary>
public class DmCardAudioPropertiesConfig
{
/// <summary>
/// The level to set on the output
/// </summary>
[JsonProperty("outLevel")]
public int OutLevel { get; set; }
/// <summary>
/// Defines if this level is adjustable or not
/// </summary>
[JsonProperty("isVolumeControlPoint")]
public bool IsVolumeControlPoint { get; set; }
}

View File

@@ -0,0 +1,82 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "DmChassisController Properties Config Schema",
"description": "",
"$ref":"EssentialsConfigSchema.json#definitions/Device",
"properties":{
"properties":{
"$ref":"#/definitions/propertiesConfig"
}
},
"definitions": {
"propertiesConfig": {
"type":"object",
"additionalProperties": true,
"properties": {
"control":{
"type":"object",
"$ref":"ControlPropertiesConfigSchema.json#definitions/ControlPropertiesConfig"
},
"volumeControls":{
"title": "Volume Controls",
"type":"object",
"additionalProperties": {
"type":"object",
"$ref": "#definitions/dmAudioCardPropertiesConfig"
}
},
"inputSlots":{
"title": "Input Slots",
"type":"object",
"additionalProperties": {
"type":"string"
}
},
"outputSlots":{
"title": "Output Slots",
"type":"object",
"additionalProperties": {
"type":"string"
}
},
"inputNames":{
"title": "Input Names",
"type":"object",
"additionalProperties": {
"type":"string"
}
},
"outputNames":{
"title": "Output Names",
"type":"object",
"additionalProperties": {
"type":"string"
}
},
"noRouteText":{
"title": "No Route Text",
"type":"string"
},
"inputSlotSupportsHdcp2":{
"type":"object",
"additionalProperties": {
"type":"boolean"
}
}
}
},
"dmAudioCardPropertiesConfig":{
"type":"object",
"properties": {
"OutLevel":{
"title": "Output Level",
"type":"integer"
},
"isVolumeControlPoint":{
"title": "Volume Control Point?",
"type":"boolean"
}
}
}
}
}

View File

@@ -103,10 +103,10 @@
<Compile Include="Chassis\DmpsRoutingController.cs" />
<Compile Include="Chassis\HdMdNxM4kEController.cs" />
<Compile Include="IDmSwitch.cs" />
<Compile Include="Config\DmpsRoutingConfig.cs" />
<Compile Include="Chassis\Config\DmpsRoutingConfig.cs" />
<Compile Include="Config\DmRmcConfig.cs" />
<Compile Include="Config\DmTxConfig.cs" />
<Compile Include="Config\DMChassisConfig.cs" />
<Compile Include="Chassis\Config\DMChassisConfig.cs" />
<Compile Include="Config\HdMdNxM4kEPropertiesConfig.cs" />
<Compile Include="Config\InputPropertiesConfig.cs" />
<Compile Include="DmPortName.cs" />
@@ -148,6 +148,9 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="VideoStatusHelpers.cs" />
<None Include="app.config" />
<EmbeddedResource Include="Chassis\Config\Schema\DmChassisControllerPropertiesConfigSchema.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Include="Properties\ControlSystem.cfg" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SamsungMDC Properties Config Schema v1",
"description": "",
"$ref":"EssentialsConfigSchema.json#/definitions/Device",
"properties":{
"properties":{
"$ref":"#/definitions/propertiesConfig"
}
},
"definitions": {
"propertiesConfig": {
"type":"object",
"additionalProperties": true,
"properties": {
"control":{
"type":"object",
"$ref":"ControlPropertiesConfigSchema.json#definitions/ControlPropertiesConfig"
},
"id":{
"title":"Display ID",
"description": "This must match the ID set in the display's on screen menu",
"type":"string",
"pattern": "^(?!FF)[0-9,A-F,a-f][0-9,A-F,a-f]$"
}
},
"required": [
"control",
"id"
]
}
}
}

View File

@@ -182,6 +182,9 @@
<Compile Include="VideoCodec\ZoomRoom\ZoomRoom.cs" />
<Compile Include="VideoCodec\ZoomRoom\ZoomRoomCamera.cs" />
<Compile Include="VideoCodec\ZoomRoom\ZoomRoomPropertiesConfig.cs" />
<EmbeddedResource Include="Display\Schema\SamsungMDCPropertiesConfigSchema.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<None Include="Properties\ControlSystem.cfg" />
</ItemGroup>
<ItemGroup>