From 96b1cbd50b74bb4b87acb7c2dd5f9b95a7e62c8a Mon Sep 17 00:00:00 2001 From: Alexander van Delft Date: Fri, 20 Dec 2024 12:33:46 +0100 Subject: [PATCH] [Add] MultiModelEditor --- COMET.Web.Common/Utilities/CopyCreator.cs | 96 +++++ .../Utilities/CopyElementDefinitionCreator.cs | 185 ++++++++++ COMET.Web.Common/Utilities/ThingCreator.cs | 334 ++++++++++++++++++ .../Components/OpenModelViewModel.cs | 5 + COMET.Web.Common/wwwroot/css/styles.css | 11 +- .../ElementDefinitionTree.razor | 92 +++++ .../ElementDefinitionTree.razor.cs | 256 ++++++++++++++ .../ElementDefinitionTree.razor.css | 5 + .../MultiModelEditor/MultiModelEditor.razor | 96 +++++ .../MultiModelEditor.razor.cs | 244 +++++++++++++ .../MultiModelEditor.razor.css | 3 + .../Extensions/ServiceCollectionExtensions.cs | 3 + COMETwebapp/Model/Applications.cs | 11 + COMETwebapp/Utilities/WebAppConstantValues.cs | 5 + .../ElementDefinitionTreeViewModel.cs | 254 +++++++++++++ .../IElementDefinitionTreeViewModel.cs | 71 ++++ .../IMultiModelEditorViewModel.cs | 110 ++++++ .../MultiModelEditorViewModel.cs | 330 +++++++++++++++++ .../Rows/ElementBaseTreeRowViewModel.cs | 93 +++++ .../ElementDefinitionTreeTreeRowViewModel.cs | 103 ++++++ .../Rows/ElementUsageTreeTreeRowViewModel.cs | 60 ++++ 21 files changed, 2366 insertions(+), 1 deletion(-) create mode 100644 COMET.Web.Common/Utilities/CopyCreator.cs create mode 100644 COMET.Web.Common/Utilities/CopyElementDefinitionCreator.cs create mode 100644 COMET.Web.Common/Utilities/ThingCreator.cs create mode 100644 COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor create mode 100644 COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor.cs create mode 100644 COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor.css create mode 100644 COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor create mode 100644 COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor.cs create mode 100644 COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor.css create mode 100644 COMETwebapp/ViewModels/Components/MultiModelEditor/ElementDefinitionTreeViewModel.cs create mode 100644 COMETwebapp/ViewModels/Components/MultiModelEditor/IElementDefinitionTreeViewModel.cs create mode 100644 COMETwebapp/ViewModels/Components/MultiModelEditor/IMultiModelEditorViewModel.cs create mode 100644 COMETwebapp/ViewModels/Components/MultiModelEditor/MultiModelEditorViewModel.cs create mode 100644 COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementBaseTreeRowViewModel.cs create mode 100644 COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementDefinitionTreeTreeRowViewModel.cs create mode 100644 COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementUsageTreeTreeRowViewModel.cs diff --git a/COMET.Web.Common/Utilities/CopyCreator.cs b/COMET.Web.Common/Utilities/CopyCreator.cs new file mode 100644 index 00000000..bba23e52 --- /dev/null +++ b/COMET.Web.Common/Utilities/CopyCreator.cs @@ -0,0 +1,96 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the Starion Web Application implementation of ECSS-E-TM-10-25 +// Annex A and Annex C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMET.Web.Common.Utilities +{ + using System.Linq; + using System.Threading.Tasks; + + using CDP4Common.CommonData; + using CDP4Common.EngineeringModelData; + + using CDP4Dal; + using CDP4Dal.Operations; + using CDP4Dal.Permission; + + /// + /// The class responsible for copy operations + /// + public class CopyCreator + { + /// + /// The in which the copy is performed + /// + private readonly ISession session; + + /// + /// Initializes a new instance of the class + /// + /// The associated + public CopyCreator(ISession session) + { + this.session = session; + } + + /// + /// Perform the copy operation of an + /// + /// The to copy + /// The target container + public async Task Copy(ElementDefinition elementDefinition, Iteration targetIteration) + { + // copy the payload to this iteration + var copyOperationHelper = new CopyPermissionHelper(this.session, false); + var copyPermissionResult = await copyOperationHelper.ComputeCopyPermissionAsync(elementDefinition, targetIteration); + + if (copyPermissionResult.ErrorList.Any()) + { + await this.WriteCopyOperation(elementDefinition, targetIteration, OperationKind.CopyKeepValuesChangeOwner); + } + else if (copyPermissionResult.CopyableThings.Any()) + { + await this.WriteCopyOperation(elementDefinition, targetIteration, OperationKind.CopyKeepValuesChangeOwner); + } + } + + /// + /// Create and write the copy operation + /// + /// The to copy + /// The target container + /// The + private async Task WriteCopyOperation(Thing thingToCopy, Thing targetContainer, OperationKind operationKind) + { + var clone = thingToCopy.Clone(false); + var containerClone = targetContainer.Clone(false); + + var transactionContext = TransactionContextResolver.ResolveContext(targetContainer); + var transaction = new ThingTransaction(transactionContext, containerClone); + transaction.Copy(clone, containerClone, operationKind); + + await this.session.Write(transaction.FinalizeTransaction()); + } + } +} \ No newline at end of file diff --git a/COMET.Web.Common/Utilities/CopyElementDefinitionCreator.cs b/COMET.Web.Common/Utilities/CopyElementDefinitionCreator.cs new file mode 100644 index 00000000..a30fd89d --- /dev/null +++ b/COMET.Web.Common/Utilities/CopyElementDefinitionCreator.cs @@ -0,0 +1,185 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the Starion Web Application implementation of ECSS-E-TM-10-25 +// Annex A and Annex C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMET.Web.Common.Utilities +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + using CDP4Common.EngineeringModelData; + + using CDP4Dal; + using CDP4Dal.Operations; + + /// + /// The class responsible for copying Element Definition + /// + public class CopyElementDefinitionCreator + { + /// + /// The string added to the element definition name + /// + private const string CopyAffix = " - Copy"; + + /// + /// The in which the copy is performed + /// + private readonly ISession session; + + /// + /// The original-clone map + /// + private readonly Dictionary groupMap = new Dictionary(); + + /// + /// The original-clone map + /// + private readonly Dictionary valueSetMap = new Dictionary(); + + /// + /// Initializes a new instance of the class + /// + /// The associated + public CopyElementDefinitionCreator(ISession session) + { + this.session = session; + } + + /// + /// Perform the copy operation of an + /// + /// The to copy + /// Do we need to copy the ElementUsages also? + public async Task Copy(ElementDefinition elementDefinition, bool areUsagesCopied) + { + var iterationClone = (Iteration)elementDefinition.Container.Clone(false); + var transactionContext = TransactionContextResolver.ResolveContext(iterationClone); + var transaction = new ThingTransaction(transactionContext, iterationClone); + + var clone = elementDefinition.Clone(true); + clone.Iid = Guid.NewGuid(); + clone.Name += CopyAffix; + + if (!areUsagesCopied) + { + clone.ContainedElement.Clear(); + } + + this.ResolveReferences(elementDefinition, clone); + iterationClone.Element.Add(clone); + + transaction.CopyDeep(clone); + await this.session.Write(transaction.FinalizeTransaction()); + } + + /// + /// Resolve the references of the copy + /// + /// The original + /// The clone + private void ResolveReferences(ElementDefinition original, ElementDefinition deepClone) + { + // Order of the item in a list is should be kept when cloning + // register mapping between original and copy + for (var i = 0; i < original.ParameterGroup.Count; i++) + { + this.groupMap.Add(original.ParameterGroup[i], deepClone.ParameterGroup[i]); + } + + for (var i = 0; i < deepClone.Parameter.Count; i++) + { + var originalParameter = original.Parameter[i]; + var cloneParameter = deepClone.Parameter[i]; + + for (var j = 0; j < originalParameter.ValueSet.Count; j++) + { + this.valueSetMap.Add(originalParameter.ValueSet[j], cloneParameter.ValueSet[j]); + } + } + + for (var i = 0; i < deepClone.ContainedElement.Count; i++) + { + var originalUsage = original.ContainedElement[i]; + var cloneUsage = deepClone.ContainedElement[i]; + + for (var j = 0; j < originalUsage.ParameterOverride.Count; j++) + { + var originalOverride = originalUsage.ParameterOverride[j]; + var cloneOverride = cloneUsage.ParameterOverride[j]; + + for (var k = 0; k < originalOverride.ValueSet.Count; k++) + { + this.valueSetMap.Add(originalOverride.ValueSet[k], cloneOverride.ValueSet[k]); + } + } + } + + // Resolve references + foreach (var group in this.groupMap.Values) + { + if (group.ContainingGroup != null) + { + // use the mapped group + group.ContainingGroup = this.groupMap[group.ContainingGroup]; + } + } + + // fix the group of the cloned parameters + foreach (var parameter in deepClone.Parameter) + { + if (parameter.Group != null) + { + parameter.Group = this.groupMap[parameter.Group]; + } + + foreach (var parameterSubscription in parameter.ParameterSubscription) + { + foreach (var parameterSubscriptionValueSet in parameterSubscription.ValueSet) + { + parameterSubscriptionValueSet.SubscribedValueSet = + this.valueSetMap[parameterSubscriptionValueSet.SubscribedValueSet]; + } + } + } + + // fix the references of the subscription value set + foreach (var elementUsage in deepClone.ContainedElement) + { + foreach (var parameterOverride in elementUsage.ParameterOverride) + { + foreach (var parameterSubscription in parameterOverride.ParameterSubscription) + { + foreach (var parameterSubscriptionValueSet in parameterSubscription.ValueSet) + { + parameterSubscriptionValueSet.SubscribedValueSet = + this.valueSetMap[parameterSubscriptionValueSet.SubscribedValueSet]; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/COMET.Web.Common/Utilities/ThingCreator.cs b/COMET.Web.Common/Utilities/ThingCreator.cs new file mode 100644 index 00000000..95f1f814 --- /dev/null +++ b/COMET.Web.Common/Utilities/ThingCreator.cs @@ -0,0 +1,334 @@ +// ---------------// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2023-2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine +// +// This file is part of CDP4-COMET WEB Community Edition +// The CDP4-COMET WEB Community Edition is the Starion Web Application implementation of ECSS-E-TM-10-25 +// Annex A and Annex C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMET.Web.Common.Utilities +{ + using System; + using System.Linq; + using System.Threading.Tasks; + + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using CDP4Dal; + using CDP4Dal.Operations; + + /// + /// The purpose of the is to encapsulate create logic for different things + /// + public class ThingCreator + { + /// + /// Create a new + /// + /// + /// The container of the that is to be created. + /// + /// + /// The that the is to be grouped in. + /// + /// + /// The that the new references + /// + /// + /// The that the references in case the is a + /// + /// + /// The that is the owner of the that is to be created. + /// + /// + /// The in which the current is to be added + /// + public async Task CreateParameter(ElementDefinition elementDefinition, ParameterGroup group, ParameterType parameterType, MeasurementScale measurementScale, DomainOfExpertise owner, ISession session) + { + if (elementDefinition == null) + { + throw new ArgumentNullException(nameof(elementDefinition), "The container ElementDefinition may not be null"); + } + + if (parameterType == null) + { + throw new ArgumentNullException(nameof(parameterType), "The ParameterType may not be null"); + } + + if (owner == null) + { + throw new ArgumentNullException(nameof(owner), "The owner DomainOfExpertise may not be null"); + } + + if (session == null) + { + throw new ArgumentNullException(nameof(session), "The session may not be null"); + } + + var parameter = new Parameter(Guid.NewGuid(), null, null) + { + Owner = owner, + ParameterType = parameterType, + Scale = measurementScale, + Group = group + }; + + var clone = elementDefinition.Clone(false); + clone.Parameter.Add(parameter); + + var transactionContext = TransactionContextResolver.ResolveContext(elementDefinition); + var transaction = new ThingTransaction(transactionContext, clone); + transaction.Create(parameter); + + try + { + var operationContainer = transaction.FinalizeTransaction(); + await session.Write(operationContainer); + } + catch (Exception ex) + { + throw; + } + } + + /// + /// Create a new + /// + /// + /// The container of the that is to be created. + /// + /// + /// The that the new references. + /// + /// + /// The in which the new is to be added + /// + public async Task CreateUserRuleVerification(RuleVerificationList ruleVerificationList, Rule rule, ISession session) + { + if (ruleVerificationList == null) + { + throw new ArgumentNullException(nameof(ruleVerificationList), "The ruleVerificationList must not be null"); + } + + if (rule == null) + { + throw new ArgumentNullException(nameof(rule), "The rule must not be null"); + } + + if (session == null) + { + throw new ArgumentNullException(nameof(session), "The session may not be null"); + } + + var userRuleVerification = new UserRuleVerification(Guid.NewGuid(), null, null) + { + Rule = rule, + IsActive = false, + Status = RuleVerificationStatusKind.NONE + }; + + var clone = ruleVerificationList.Clone(false); + clone.RuleVerification.Add(userRuleVerification); + + var transactionContext = TransactionContextResolver.ResolveContext(ruleVerificationList); + var transaction = new ThingTransaction(transactionContext, clone); + transaction.Create(userRuleVerification); + + try + { + var operationContainer = transaction.FinalizeTransaction(); + await session.Write(operationContainer); + } + catch (Exception ex) + { + throw; + } + } + + /// + /// Create a new + /// + /// + /// The container of the that is to be created. + /// + /// + /// The name for the + /// + /// + /// The in which the new is to be added + /// + public async Task CreateBuiltInRuleVerification(RuleVerificationList ruleVerificationList, string name, ISession session) + { + if (ruleVerificationList == null) + { + throw new ArgumentNullException(nameof(ruleVerificationList), "The ruleVerificationList must not be null"); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The name may not be null or empty"); + } + + if (session == null) + { + throw new ArgumentNullException(nameof(session), "The session may not be null"); + } + + var builtInRuleVerification = new BuiltInRuleVerification(Guid.NewGuid(), null, null) + { + Name = name, + IsActive = false, + Status = RuleVerificationStatusKind.NONE + }; + + var clone = ruleVerificationList.Clone(false); + clone.RuleVerification.Add(builtInRuleVerification); + + var transactionContext = TransactionContextResolver.ResolveContext(ruleVerificationList); + var transaction = new ThingTransaction(transactionContext, clone); + transaction.Create(builtInRuleVerification); + + try + { + var operationContainer = transaction.FinalizeTransaction(); + await session.Write(operationContainer); + } + catch (Exception ex) + { + throw; + } + } + + /// + /// Create a new + /// + /// + /// The container of the that is to be created. + /// + /// + /// The referenced of the that is to be created. + /// + /// + /// The that is the owner of the that is to be created. + /// + /// + /// The in which the current is to be added + /// + public async Task CreateElementUsage(ElementDefinition container, ElementDefinition referencedDefinition, DomainOfExpertise owner, ISession session) + { + if (container == null) + { + throw new ArgumentNullException(nameof(container), "The container must not be null"); + } + + if (referencedDefinition == null) + { + throw new ArgumentNullException(nameof(referencedDefinition), "The referencedDefinition must not be null"); + } + + if (owner == null) + { + throw new ArgumentNullException(nameof(owner), "The owner must not be null"); + } + + if (session == null) + { + throw new ArgumentNullException(nameof(session), "The session may not be null"); + } + + var clone = container.Clone(false); + var usage = new ElementUsage + { + Name = referencedDefinition.Name, + ShortName = referencedDefinition.ShortName, + Owner = owner, + ElementDefinition = referencedDefinition + }; + + clone.ContainedElement.Add(usage); + + var transactionContext = TransactionContextResolver.ResolveContext(container); + var transaction = new ThingTransaction(transactionContext, clone); + transaction.Create(usage); + + try + { + var operationContainer = transaction.FinalizeTransaction(); + await session.Write(operationContainer); + } + catch (Exception ex) + { + throw; + } + } + + /// + /// Method for creating a for requirement verification between a and a . + /// + /// The for which the will be created + /// The for which the will be created + /// The that acts as the source of the + /// The that acts as the target of the + /// An awaitable + public async Task CreateBinaryRelationshipForRequirementVerification(ISession session, Iteration iteration, ParameterOrOverrideBase parameter, RelationalExpression relationalExpression) + { + session.OpenIterations.TryGetValue(iteration, out var tuple); + + var binaryRelationship = new BinaryRelationship(Guid.NewGuid(), null, null) { Owner = tuple?.Item1 }; + + var transaction = new ThingTransaction(TransactionContextResolver.ResolveContext(relationalExpression)); + + binaryRelationship.Container = iteration; + binaryRelationship.Source = parameter; + binaryRelationship.Target = relationalExpression; + + var iterationClone = iteration.Clone(false); + iterationClone.Relationship.Add(binaryRelationship); + transaction.CreateOrUpdate(iterationClone); + transaction.Create(binaryRelationship); + + try + { + var operationContainer = transaction.FinalizeTransaction(); + await session.Write(operationContainer); + } + catch (Exception ex) + { + } + } + + /// + /// Checks if creating a for requirement verification is allowed for these two objects + /// + /// The + /// The + /// True if creation is allowed + public bool IsCreateBinaryRelationshipForRequirementVerificationAllowed(ParameterOrOverrideBase parameter, RelationalExpression relationalExpression) + { + return (parameter.ParameterType.Iid == relationalExpression.ParameterType.Iid) && + (!(parameter.ParameterType is QuantityKind) || (parameter.Scale == relationalExpression.Scale)) && + !relationalExpression.QueryRelationships + .Any( + x => x is BinaryRelationship relationship + && (relationship.Source.Iid == parameter.Iid)); + } + } +} diff --git a/COMET.Web.Common/ViewModels/Components/OpenModelViewModel.cs b/COMET.Web.Common/ViewModels/Components/OpenModelViewModel.cs index 170f5ddb..d256e177 100644 --- a/COMET.Web.Common/ViewModels/Components/OpenModelViewModel.cs +++ b/COMET.Web.Common/ViewModels/Components/OpenModelViewModel.cs @@ -222,6 +222,11 @@ public virtual async Task> OpenSession() return Result.Fail(["The selected iteration and the domain of expertise should not be null"]); } + if (this.sessionService.OpenIterations.Items.Any(x => x.IterationSetup.Iid == this.SelectedIterationSetup.IterationSetupId)) + { + return Result.Fail(["The selected iteration is already openened"]); + } + this.IsOpeningSession = true; var result = await this.sessionService.ReadIteration(this.SelectedEngineeringModel.IterationSetup diff --git a/COMET.Web.Common/wwwroot/css/styles.css b/COMET.Web.Common/wwwroot/css/styles.css index 83c84789..c82e3775 100644 --- a/COMET.Web.Common/wwwroot/css/styles.css +++ b/COMET.Web.Common/wwwroot/css/styles.css @@ -606,4 +606,13 @@ left: 5px; /* Position of the icon */ color: #757575; /* Icon color */ font-size: 16px; /* Icon size */ - } \ No newline at end of file + } + +.treeview-drag-over { + background-color: burlywood; +} + +.treeview-item-drag-over { + background-color: burlywood; + box-shadow: 0px 0px 2px 10px burlywood; +} \ No newline at end of file diff --git a/COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor b/COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor new file mode 100644 index 00000000..9327cfdd --- /dev/null +++ b/COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor @@ -0,0 +1,92 @@ +@using COMET.Web.Common.Model +@using COMETwebapp.ViewModels.Components.MultiModelEditor.Rows +@using CDP4JsonSerializer.JsonConverter +@inject IJSRuntime JSRuntime + +
+@if (this.IsModelSelectionEnabled) +{ + + +} +else +{ +
@this.ViewModel.Description
+} +
+
+ Drop here to create new element... +
+ + + @{ + var dataItem = (ElementBaseTreeRowViewModel)context.DataItem; + var draggable = this.AllowDrag && this.AllowNodeDrag.Invoke(this, dataItem); + var isTopElement = dataItem is ElementDefinitionTreeTreeRowViewModel { IsTopElement: true }; + + var style = isTopElement ? "font-weight-bold" : string.Empty; + } + + @if (draggable) + { +
+ @dataItem.ElementName +
+ } + else + { +
+ @dataItem.ElementName +
+ } +
+ + + + +
diff --git a/COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor.cs b/COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor.cs new file mode 100644 index 00000000..f700d27f --- /dev/null +++ b/COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor.cs @@ -0,0 +1,256 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The COMET WEB Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The COMET WEB Community Edition is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMETwebapp.Components.MultiModelEditor +{ + using CDP4Common.EngineeringModelData; + + using COMETwebapp.ViewModels.Components.MultiModelEditor; + using COMETwebapp.ViewModels.Components.MultiModelEditor.Rows; + + using DevExpress.Blazor; + + using Microsoft.AspNetCore.Components; + + /// + /// Support class for the component + /// + public partial class ElementDefinitionTree + { + /// + /// The injected + /// + [Inject] + public IElementDefinitionTreeViewModel ViewModel { get; set; } + + /// + /// The Iteration + /// + [Parameter] + public Iteration Iteration { get; set; } + + /// + /// Gets or sets a value indicating that another model can be selected for this TreeView or not + /// + [Parameter] + public bool IsModelSelectionEnabled { get; set; } + + /// + /// Fires after node selection has been changed for a specific item. + /// + [Parameter] + public EventCallback SelectionChanged { get; set; } + + /// + /// Fires after node dragging has been started for a specific item. + /// + [Parameter] + public EventCallback<(ElementDefinitionTree, ElementBaseTreeRowViewModel)> OnDragStart { get; set; } = new(); + + /// + /// Fires after node dragging has been ended for a specific item. + /// + [Parameter] + public EventCallback<(ElementDefinitionTree, ElementBaseTreeRowViewModel)> OnDragEnd { get; set; } = new(); + + /// + /// Fires after node drop has been executed on a specific item. + /// + [Parameter] + public EventCallback<(ElementDefinitionTree, ElementBaseTreeRowViewModel)> OnDrop { get; set; } = new(); + + /// + /// Fires after node drag-over has been started for a specific item. + /// + [Parameter] + public EventCallback<(ElementDefinitionTree, object)> OnDragEnter { get; set; } = new(); + + /// + /// Fires after node drag-over has been ended for a specific item. + /// + [Parameter] + public EventCallback<(ElementDefinitionTree, object)> OnDragLeave { get; set; } = new(); + + /// + /// Fires when calculation of is necessary + /// + [Parameter] + public EventCallback OnCalculateDropIsAllowed { get; set; } = new(); + + /// + /// Is evaluated when calculation if a node is draggable is necessary + /// + [Parameter] + public Func AllowNodeDrag { get; set; } = (x, y) => true; + + /// + /// Gets or sets a value indicating that dragging a node is allowed for this + /// + [Parameter] + public bool AllowDrag { get; set; } + + /// + /// Gets or sets a value indicating that dropping a node is allowed for this + /// + [Parameter] + public bool AllowDrop { get; set; } + + /// + /// Holds a reference to the object where the dragged node is dragged over + /// + private object dragOverNode; + + /// + /// Gets or sets a value indicating that dropping a node is allowed for this + /// + public bool AllowNodeDrop { get; set; } + + /// + /// A collection of + /// + public IEnumerable Rows { get; set; } + + /// + /// Gets or sets the + /// + public DxTreeView TreeView { get; set; } + + /// + /// Method invoked after each time the component has been rendered. Note that the component does + /// not automatically re-render after the completion of any returned , because + /// that would cause an infinite render loop. + /// + /// + /// Set to true if this is the first time has been invoked + /// on this component instance; otherwise false. + /// + /// A representing any asynchronous operation. + /// + /// The and lifecycle methods + /// are useful for performing interop, or interacting with values received from @ref. + /// Use the parameter to ensure that initialization work is only performed + /// once. + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (this.ViewModel.Iteration != this.Iteration) + { + this.Iteration = this.ViewModel.Iteration; + } + } + + /// + /// Method invoked when the component has received parameters from its parent in + /// the render tree, and the incoming values have been assigned to properties. + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + this.ViewModel.Iteration = this.Iteration; + } + + /// + /// Clears the selected node(s) in this + /// + public void ClearSelection() + { + this.TreeView.ClearSelection(); + } + + /// + /// Is executed when dragging of a node has started + /// + /// The node where dragging has been started for + /// an awaitable + private async Task DragStart(ElementBaseTreeRowViewModel node) + { + await this.OnDragStart.InvokeAsync((this, node)); + await this.OnCalculateDropIsAllowed.InvokeAsync(this); + Console.WriteLine("DragStart"); + } + + /// + /// Is executed when dragging of a node has ended + /// + /// The node where dragging has been ended for + /// an awaitable + private async Task DragEnd(ElementBaseTreeRowViewModel node) + { + await this.OnDragEnd.InvokeAsync((this, node)); + await this.OnCalculateDropIsAllowed.InvokeAsync(this); + Console.WriteLine("DragEnd"); + } + + /// + /// Is executed when a node has been dropped onto another node + /// + /// The node where the dragged node has been dropped onto + /// an awaitable + private async Task Drop(ElementBaseTreeRowViewModel node) + { + this.dragOverNode = null; + + if (this.AllowDrop) + { + await this.OnDrop.InvokeAsync((this, node)); + await this.OnCalculateDropIsAllowed.InvokeAsync(this); + Console.WriteLine("Drop"); + } + } + + /// + /// Is executed when a dragged node hover over another droppable node + /// + /// The node where the dragged node has been hovered over + /// an awaitable + private async Task DragEnter(object node) + { + if (this.AllowDrop) + { + this.dragOverNode = node; + await this.OnDragEnter.InvokeAsync((this, node)); + await this.OnCalculateDropIsAllowed.InvokeAsync(this); + + Console.WriteLine("DragEnter"); + } + } + + /// + /// Is executed when a dragged node is not hovered over another droppable node anymore + /// + /// The node where the dragged node had been hovered over + /// an awaitable + private async Task DragLeave(object node) + { + if (this.AllowDrop) + { + this.dragOverNode = null; + await this.OnDragLeave.InvokeAsync((this, node)); + await this.OnCalculateDropIsAllowed.InvokeAsync(this); + Console.WriteLine("DragLeave"); + } + } + } +} diff --git a/COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor.css b/COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor.css new file mode 100644 index 00000000..bf169aed --- /dev/null +++ b/COMETwebapp/Components/MultiModelEditor/ElementDefinitionTree.razor.css @@ -0,0 +1,5 @@ +.sticky-scrollable-column { + position: sticky; + top: 0px; + overflow: hidden; +} diff --git a/COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor b/COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor new file mode 100644 index 00000000..9f6f1d0f --- /dev/null +++ b/COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor @@ -0,0 +1,96 @@ + + +@using COMETwebapp.ViewModels.Components.MultiModelEditor.Rows +@inherits SingleIterationApplicationBase; + + + +
+
+
+

Source Model

+ + +
+
+

Target Model

+ + +
+
+ +
+
+ @if (this.ViewModel.SelectedElementDefinition is not null) + { + + } + + +
+
+
+ +
+
+
+ + + + + + + + + + + +
diff --git a/COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor.cs b/COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor.cs new file mode 100644 index 00000000..32c4a3c4 --- /dev/null +++ b/COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor.cs @@ -0,0 +1,244 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The COMET WEB Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The COMET WEB Community Edition is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMETwebapp.Components.MultiModelEditor +{ + using CDP4Common.EngineeringModelData; + + using COMET.Web.Common.Components.Applications; + using COMET.Web.Common.Extensions; + + using COMETwebapp.ViewModels.Components.ModelEditor.Rows; + using COMETwebapp.ViewModels.Components.MultiModelEditor.Rows; + + using DevExpress.Blazor; + + using ReactiveUI; + + /// + /// Support class for the component + /// + public partial class MultiModelEditor + { + /// + /// Holds a reference to the data of the node where another node is dragged over + /// + private (ElementDefinitionTree, object) DragOverObject; + + /// + /// Holds a reference to the data of the node that is currently dragged + /// + private (ElementDefinitionTree, ElementBaseTreeRowViewModel) DragObject; + + /// + /// The validation messages to display + /// + private string ErrorMessage { get; set; } + + /// + /// Gets or sets the source tree component + /// + public ElementDefinitionTree SourceTree { get; set; } + + /// + /// Gets or sets the target tree component + /// + public ElementDefinitionTree TargetTree { get; set; } + + /// + /// Handles the post-assignement flow of the property + /// + protected override void OnViewModelAssigned() + { + base.OnViewModelAssigned(); + + this.Disposables.Add(this.WhenAnyValue(x => x.ViewModel.IsOnCreationMode).SubscribeAsync(_ => this.InvokeAsync(this.StateHasChanged))); + this.Disposables.Add(this.WhenAnyValue(x => x.ViewModel.IsOnAddingParameterMode).SubscribeAsync(_ => this.InvokeAsync(this.StateHasChanged))); + } + + /// + /// Initializes values of the component and of the ViewModel based on parameters provided from the url + /// + /// A for parameters + protected override void InitializeValues(Dictionary parameters) + { + } + + /// + /// Method executed when a is selected + /// + /// The + private void OnElementSelected(ElementBaseTreeRowViewModel args) + { + this.ViewModel.SelectElement(args?.ElementBase); + } + + /// + /// Is executed when dragging has been started for a specific node () in a specific + /// + /// A that contains the specific and the specific node () + /// an awaitable + private Task OnDragStart((ElementDefinitionTree, ElementBaseTreeRowViewModel) nodeData) + { + this.DragObject = nodeData; + return Task.CompletedTask; + } + + /// + /// Is executed when dragging has been ended for a specific node () in a specific + /// + /// A that contains the specific and the specific node () + /// an awaitable + private Task OnDragEnd((ElementDefinitionTree, ElementBaseTreeRowViewModel) nodeData) + { + this.DragObject = (null, null); + return Task.CompletedTask; + } + + /// + /// Is executed when a dragged node () has been dropped onto another element in a specific + /// + /// A that contains the specific and the specific node () + /// an awaitable + private async Task OnDrop((ElementDefinitionTree, ElementBaseTreeRowViewModel) nodeData) + { + this.ErrorMessage = string.Empty; + + if (this.DragObject.Item2 is not ElementDefinitionTreeTreeRowViewModel elementDefinitionTreeRowViewModel) + { + return; + } + + try + { + if (nodeData.Item2 == null) + { + // Drop in the same model + await this.ViewModel.CopyAndAddNewElement(elementDefinitionTreeRowViewModel.ElementBase); + } + else + { + await this.ViewModel.AddNewElementUsage(elementDefinitionTreeRowViewModel.ElementBase, nodeData.Item2.ElementBase); + } + } + catch (Exception ex) + { + this.ErrorMessage = ex.Message; + } + finally + { + this.DragOverObject = (null, null); + this.DragObject = (null, null); + + this.StateHasChanged(); + } + } + + /// + /// Is executed when a dragged node () hovers over a specific element () in a specific + /// + /// A that contains the specific and the specific element () + /// an awaitable + private Task OnDragEnter((ElementDefinitionTree, object) elementData) + { + this.DragOverObject = elementData; + return Task.CompletedTask; + } + + /// + /// Is executed when a dragged node () leaves a previously hovered over specific element () in a specific + /// + /// A that contains the specific and the specific element () + /// an awaitable + private Task OnDragLeave((ElementDefinitionTree, object) elementData) + { + this.DragOverObject = (null, null); + return Task.CompletedTask; + } + + /// + /// Sets the AllowNodeDrop property for a specific node in a , based on the calculated data for and + /// + /// The to calculate this for + /// an awaitable + private Task SetDropIsAllowed(ElementDefinitionTree elementDefinitionTree) + { + elementDefinitionTree.AllowNodeDrop = this.CalculateDropIsAllowed(elementDefinitionTree); + return Task.CompletedTask; + } + + /// + /// Calculates is dropping of a dragged node () is allowed onto the node where it hovers over () + /// + /// The where to calculate for + /// A value indicating is dropping is actually allowed + private bool CalculateDropIsAllowed(ElementDefinitionTree elementDefinitionTree) + { + var dragOverObject = this.DragOverObject; + var dragObject = this.DragObject; + + if (elementDefinitionTree.AllowDrop) + { + if (dragObject != (null, null)) + { + if (dragOverObject == dragObject) + { + return false; + } + + if (dragOverObject.Item2 is ElementDefinitionTreeTreeRowViewModel dragOverVm) + { + if (dragObject.Item2 is ElementDefinitionTreeTreeRowViewModel dragVm) + { + if (dragOverVm.ElementBase == dragVm.ElementBase) + { + return false; + } + + if (dragOverVm.ElementBase.GetContainerOfType() == dragVm.ElementBase.GetContainerOfType()) + { + return true; + } + + return false; + } + + return false; + } + + if (dragOverObject.Item1 == elementDefinitionTree) + { + return true; + } + + return false; + } + + return false; + } + + return false; + } + } +} diff --git a/COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor.css b/COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor.css new file mode 100644 index 00000000..bc45ff21 --- /dev/null +++ b/COMETwebapp/Components/MultiModelEditor/MultiModelEditor.razor.css @@ -0,0 +1,3 @@ +.sticky-scrollable-column { + max-height: 80vh; +} diff --git a/COMETwebapp/Extensions/ServiceCollectionExtensions.cs b/COMETwebapp/Extensions/ServiceCollectionExtensions.cs index b9127ddd..99cc9f37 100644 --- a/COMETwebapp/Extensions/ServiceCollectionExtensions.cs +++ b/COMETwebapp/Extensions/ServiceCollectionExtensions.cs @@ -57,6 +57,7 @@ namespace COMETwebapp.Extensions using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileHandler; using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FileRevisionHandler; using COMETwebapp.ViewModels.Components.EngineeringModel.FileStore.FolderHandler; + using COMETwebapp.ViewModels.Components.MultiModelEditor; using COMETwebapp.ViewModels.Components.ParameterEditor.BatchParameterEditor; using COMETwebapp.ViewModels.Components.ReferenceData; using COMETwebapp.ViewModels.Pages; @@ -104,6 +105,8 @@ public static void RegisterViewModels(this IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); diff --git a/COMETwebapp/Model/Applications.cs b/COMETwebapp/Model/Applications.cs index b17029b7..5f1a2aed 100644 --- a/COMETwebapp/Model/Applications.cs +++ b/COMETwebapp/Model/Applications.cs @@ -30,6 +30,7 @@ namespace COMETwebapp.Model using COMETwebapp.Components.EngineeringModel; using COMETwebapp.Components.ModelDashboard; using COMETwebapp.Components.ModelEditor; + using COMETwebapp.Components.MultiModelEditor; using COMETwebapp.Components.ParameterEditor; using COMETwebapp.Components.ReferenceData; using COMETwebapp.Components.SiteDirectory; @@ -103,6 +104,16 @@ private static List InitializesApplications() ComponentType = typeof(ElementDefinitionTable) }, + new TabbedApplication + { + Name = "Multi Model Editor", + Color = "#76fd98", + IconType = typeof(FeatherBox), + Description = "Populate multiple models", + Url = WebAppConstantValues.MultiModelEditorPage, + ComponentType = typeof(MultiModelEditor) + }, + new TabbedApplication { Name = "Parameter Editor", diff --git a/COMETwebapp/Utilities/WebAppConstantValues.cs b/COMETwebapp/Utilities/WebAppConstantValues.cs index cf450af4..2f36c412 100644 --- a/COMETwebapp/Utilities/WebAppConstantValues.cs +++ b/COMETwebapp/Utilities/WebAppConstantValues.cs @@ -126,6 +126,11 @@ public static class WebAppConstantValues /// public const string ModelEditorPage = "ModelEditor"; + /// + /// The page name of the Model Editor + /// + public const string MultiModelEditorPage = "MultiModelEditor"; + /// /// The page nastringthe Book Editor /// diff --git a/COMETwebapp/ViewModels/Components/MultiModelEditor/ElementDefinitionTreeViewModel.cs b/COMETwebapp/ViewModels/Components/MultiModelEditor/ElementDefinitionTreeViewModel.cs new file mode 100644 index 00000000..ac849a57 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/MultiModelEditor/ElementDefinitionTreeViewModel.cs @@ -0,0 +1,254 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The COMET WEB Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The COMET WEB Community Edition is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMETwebapp.ViewModels.Components.MultiModelEditor +{ + using System.Collections.ObjectModel; + + using CDP4Common.CommonData; + using CDP4Common.EngineeringModelData; + + using CDP4Dal; + using CDP4Dal.Events; + + using COMET.Web.Common.Model; + using COMET.Web.Common.Services.SessionManagement; + using COMET.Web.Common.ViewModels.Components.Applications; + + using COMETwebapp.Components.ModelEditor; + using COMETwebapp.Components.MultiModelEditor; + using COMETwebapp.ViewModels.Components.ModelEditor; + using COMETwebapp.ViewModels.Components.MultiModelEditor.Rows; + + using DynamicData; + + using ReactiveUI; + + /// + /// ViewModel for the + /// + public class ElementDefinitionTreeViewModel : ApplicationBaseViewModel, IElementDefinitionTreeViewModel + { + /// + /// The + /// + private readonly ISessionService sessionService; + + /// + /// Backing field for + /// + private Iteration iteration; + + /// + /// Gets the Description of the selected model and iteration + /// + public string Description => this.selectedIterationData?.IterationName ?? "Please select a model"; + + /// + /// Backing field for the property + /// + private IterationData selectedIterationData; + + /// + /// Creates a new instance of + /// + /// the + /// The + public ElementDefinitionTreeViewModel(ISessionService sessionService, ICDPMessageBus messageBus) : base(sessionService, messageBus) + { + this.sessionService = sessionService; + this.RegisterViewModelWithReusableRows(this); + this.Iterations.Add(null); + + this.Disposables.Add(this.WhenAnyValue(x => x.SelectedIterationData).Subscribe(x => this.Iteration = this.sessionService.OpenIterations.Items.SingleOrDefault(y => y.IterationSetup.Iid == x?.IterationSetupId))); + + this.Disposables.Add(this.WhenAnyValue(x => x.Iteration).Subscribe(x => + { + this.Rows.Clear(); + + if (x != null) + { + this.AddRows(x.Element); + this.SelectedIterationData = this.Iterations.SingleOrDefault(y => y?.IterationSetupId == x.IterationSetup.Iid); + } + })); + + this.Disposables.Add(this.sessionService.OpenIterations.Connect().Subscribe(this.RefreshIterations)); + + this.InitializeSubscriptions([typeof(ElementBase)]); + } + + /// + /// The from which to build the tree + /// + public Iteration Iteration + { + get => this.iteration; + set => this.RaiseAndSetIfChanged(ref this.iteration, value); + } + + /// + /// The from which to build the tree + /// + public IterationData SelectedIterationData + { + get => this.selectedIterationData; + set => this.RaiseAndSetIfChanged(ref this.selectedIterationData, value); + } + + /// + /// Gets or a collection of selectable s + /// + public ObservableCollection Iterations { get; } = new(); + + /// + /// All of the iteration + /// + public List Elements { get; set; } = []; + + /// + /// Gets the collection of the for target model + /// + public ObservableCollection Rows { get; set; } = []; + + /// + /// Represents the selected ElementDefinitionRowViewModel + /// + public ElementDefinition SelectedElementDefinition { get; set; } + + /// + /// Add rows related to that has been added + /// + /// A collection of added + public void AddRows(IEnumerable addedThings) + { + var listOfAddedElementBases = addedThings.OfType().Where(x => this.Iteration?.Element.Contains(x) ?? false).ToList(); + this.Rows.AddRange(listOfAddedElementBases.Select(e => new ElementDefinitionTreeTreeRowViewModel(e))); + } + + /// + /// Updates rows related to that have been updated + /// + /// A collection of updated + public void UpdateRows(IEnumerable updatedThings) + { + foreach (var element in updatedThings.OfType().Where(x => this.Iteration?.Element.Contains(x) ?? false).ToList()) + { + var row = this.Rows.FirstOrDefault(x => x.ElementBase.Iid == element.Iid); + row?.UpdateProperties(new ElementDefinitionTreeTreeRowViewModel(element)); + } + } + + /// + /// Remove rows related to a that has been deleted + /// + /// A collection of deleted + public void RemoveRows(IEnumerable deletedThings) + { + foreach (var elementId in deletedThings.OfType().Select(x => x.Iid)) + { + var row = this.Rows.FirstOrDefault(x => x.ElementBase.Iid == elementId); + + if (row != null) + { + this.Rows.Remove(row); + } + } + } + + /// + /// Handles the message received + /// + /// A + protected override async Task OnEndUpdate() + { + await this.OnSessionRefreshed(); + } + + /// + /// Handles the refresh of the current + /// + /// A + protected override async Task OnSessionRefreshed() + { + if (this.AddedThings.Count == 0 && this.DeletedThings.Count == 0 && this.UpdatedThings.Count == 0) + { + return; + } + + this.IsLoading = true; + await Task.Delay(1); + + this.UpdateInnerComponents(); + this.ClearRecordedChanges(); + this.IsLoading = false; + } + + /// + /// Refreshes the property + /// + /// The containing all the necessary changes + private void RefreshIterations(IChangeSet changeSet) + { + foreach (var change in changeSet) + { + switch (change.Reason) + { + case ListChangeReason.AddRange: + foreach (var changeItem in change.Range) + { + var newChangeIterationData = new IterationData(changeItem.IterationSetup, true); + + if (!this.Iterations.Contains(newChangeIterationData)) + { + this.Iterations.Add(newChangeIterationData); + } + } + + break; + + case ListChangeReason.Add: + var newIterationData = new IterationData(change.Item.Current.IterationSetup, true); + + if (!this.Iterations.Contains(newIterationData)) + { + this.Iterations.Add(newIterationData); + } + + break; + + case ListChangeReason.Remove: + var currentItem = this.Iterations.FirstOrDefault(x => x?.IterationSetupId == change.Item.Current.IterationSetup.Iid); + + if (currentItem != null) + { + this.Iterations.Remove(currentItem); + } + + break; + } + } + } + } +} diff --git a/COMETwebapp/ViewModels/Components/MultiModelEditor/IElementDefinitionTreeViewModel.cs b/COMETwebapp/ViewModels/Components/MultiModelEditor/IElementDefinitionTreeViewModel.cs new file mode 100644 index 00000000..dd2fea60 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/MultiModelEditor/IElementDefinitionTreeViewModel.cs @@ -0,0 +1,71 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The COMET WEB Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The COMET WEB Community Edition is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMETwebapp.ViewModels.Components.MultiModelEditor +{ + using System.Collections.ObjectModel; + + using CDP4Common.EngineeringModelData; + + using COMET.Web.Common.Model; + using COMET.Web.Common.ViewModels.Components.Applications; + + using COMETwebapp.ViewModels.Components.MultiModelEditor.Rows; + + /// + /// Interface for the + /// + public interface IElementDefinitionTreeViewModel : IHaveReusableRows + { + /// + /// Gets the collection of the + /// + ObservableCollection Rows { get; set; } + + /// + /// The from which to build the tree + /// + Iteration Iteration { get; set; } + + /// + /// Represents the selected ElementDefinitionRowViewModel + /// + ElementDefinition SelectedElementDefinition { get; set; } + + /// + /// Gets the Description of the selected model and iteration + /// + string Description { get; } + + /// + /// Gets or a collection of selectable s + /// + ObservableCollection Iterations { get; } + + /// + /// The from which to build the tree + /// + IterationData SelectedIterationData { get; set; } + } +} diff --git a/COMETwebapp/ViewModels/Components/MultiModelEditor/IMultiModelEditorViewModel.cs b/COMETwebapp/ViewModels/Components/MultiModelEditor/IMultiModelEditorViewModel.cs new file mode 100644 index 00000000..4fb2ae07 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/MultiModelEditor/IMultiModelEditorViewModel.cs @@ -0,0 +1,110 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The COMET WEB Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The COMET WEB Community Edition is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMETwebapp.ViewModels.Components.MultiModelEditor +{ + using CDP4Common.EngineeringModelData; + + using COMET.Web.Common.ViewModels.Components.Applications; + + using COMETwebapp.Components.ModelEditor; + using COMETwebapp.ViewModels.Components.ModelEditor; + using COMETwebapp.ViewModels.Components.ModelEditor.AddParameterViewModel; + using COMETwebapp.ViewModels.Components.SystemRepresentation; + + /// + /// Interface for the + /// + public interface IMultiModelEditorViewModel : ISingleIterationApplicationBaseViewModel + { + /// + /// Gets the target /> + /// + Iteration TargetIteration { get; } + + /// + /// Gets the source + /// + Iteration SourceIteration { get; } + + /// + /// Value indicating the user is currently creating a new + /// + bool IsOnCreationMode { get; set; } + + /// + /// Represents the selected ElementDefinitionRowViewModel + /// + ElementDefinition SelectedElementDefinition { get; set; } + + /// + /// The + /// + IElementDefinitionDetailsViewModel ElementDefinitionDetailsViewModel { get; } + + /// + /// Gets the + /// + IElementDefinitionCreationViewModel ElementDefinitionCreationViewModel { get; set; } + + /// + /// Gets the + /// + IAddParameterViewModel AddParameterViewModel { get; set; } + + /// + /// Value indicating the user is currently adding a new to a + /// + bool IsOnAddingParameterMode { get; set; } + + /// + /// Opens the popup + /// + void OpenCreateElementDefinitionCreationPopup(); + + /// + /// Set the selected + /// + /// The selected + void SelectElement(ElementBase selectedElementBase); + + /// + /// Opens the popup + /// + void OpenAddParameterPopup(); + + /// + /// Add a new based on an existing + /// + /// The + Task CopyAndAddNewElement(ElementBase elementBase); + + /// + /// Add a new based on an existing + /// + /// The to be added as + /// The where to add the new to + Task AddNewElementUsage(ElementBase fromElementBase, ElementBase toElementBase); + } +} diff --git a/COMETwebapp/ViewModels/Components/MultiModelEditor/MultiModelEditorViewModel.cs b/COMETwebapp/ViewModels/Components/MultiModelEditor/MultiModelEditorViewModel.cs new file mode 100644 index 00000000..08f58b4f --- /dev/null +++ b/COMETwebapp/ViewModels/Components/MultiModelEditor/MultiModelEditorViewModel.cs @@ -0,0 +1,330 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The COMET WEB Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The COMET WEB Community Edition is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMETwebapp.ViewModels.Components.MultiModelEditor +{ + using CDP4Common.CommonData; + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using CDP4Dal; + using CDP4Dal.Events; + + using COMET.Web.Common.Services.SessionManagement; + using COMET.Web.Common.Utilities; + using COMET.Web.Common.ViewModels.Components.Applications; + + using COMETwebapp.Components.ModelEditor; + using COMETwebapp.ViewModels.Components.ModelEditor; + using COMETwebapp.ViewModels.Components.ModelEditor.AddParameterViewModel; + using COMETwebapp.ViewModels.Components.SystemRepresentation; + using COMETwebapp.ViewModels.Components.SystemRepresentation.Rows; + + using Microsoft.AspNetCore.Components; + + using ReactiveUI; + + /// + /// ViewModel for the + /// + public class MultiModelEditorViewModel : SingleIterationApplicationBaseViewModel, IMultiModelEditorViewModel + { + /// + /// The + /// + private readonly ISessionService sessionService; + + /// + /// Backing field for + /// + private bool isOnAddingParameterMode; + + /// + /// Backing field for + /// + private bool isOnCreationMode; + + /// + /// Creates a new instance of + /// + /// the + /// The + public MultiModelEditorViewModel(ISessionService sessionService, ICDPMessageBus messageBus) : base(sessionService, messageBus) + { + this.sessionService = sessionService; + var eventCallbackFactory = new EventCallbackFactory(); + + this.ElementDefinitionCreationViewModel = new ElementDefinitionCreationViewModel(sessionService, messageBus) + { + OnValidSubmit = eventCallbackFactory.Create(this, this.AddingElementDefinition) + }; + + this.AddParameterViewModel = new ModelEditor.AddParameterViewModel.AddParameterViewModel(sessionService, messageBus) + { + OnParameterAdded = eventCallbackFactory.Create(this, () => this.IsOnAddingParameterMode = false) + }; + + this.InitializeSubscriptions([typeof(ElementBase)]); + + this.TargetIteration = this.CurrentThing; + } + + /// + /// Represents the selected ElementDefinitionRowViewModel + /// + public ElementDefinition SelectedElementDefinition { get; set; } + + /// + /// The + /// + public IElementDefinitionDetailsViewModel ElementDefinitionDetailsViewModel { get; } = new ElementDefinitionDetailsViewModel(); + + /// + /// Gets the + /// + public IElementDefinitionCreationViewModel ElementDefinitionCreationViewModel { get; set; } + + /// + /// Gets the + /// + public IAddParameterViewModel AddParameterViewModel { get; set; } + + /// + /// Gets target + /// + public Iteration TargetIteration { get; set; } + + /// + /// Gets source + /// + public Iteration SourceIteration { get; set; } + + /// + /// Value indicating the user is currently creating a new + /// + public bool IsOnCreationMode + { + get => this.isOnCreationMode; + set => this.RaiseAndSetIfChanged(ref this.isOnCreationMode, value); + } + + /// + /// Value indicating the user is currently adding a new to a + /// + public bool IsOnAddingParameterMode + { + get => this.isOnAddingParameterMode; + set => this.RaiseAndSetIfChanged(ref this.isOnAddingParameterMode, value); + } + + /// + /// Set the selected + /// + /// The selected + public void SelectElement(ElementBase selectedElementBase) + { + // It is preferable to have a selection based on the Iid of the Thing + this.ElementDefinitionDetailsViewModel.SelectedSystemNode = selectedElementBase; + + this.SelectedElementDefinition = selectedElementBase switch + { + ElementDefinition definition => definition, + ElementUsage usage => usage.ElementDefinition, + _ => null + }; + + this.ElementDefinitionDetailsViewModel.Rows = this.SelectedElementDefinition?.Parameter.Select(x => new ElementDefinitionDetailsRowViewModel(x)).ToList(); + this.AddParameterViewModel.SetSelectedElementDefinition(this.SelectedElementDefinition); + } + + /// + /// Opens the popup + /// + public void OpenCreateElementDefinitionCreationPopup() + { + this.ElementDefinitionCreationViewModel.ElementDefinition = new ElementDefinition(); + this.ElementDefinitionCreationViewModel.SelectedCategories = new List(); + this.IsOnCreationMode = true; + } + + /// + /// Opens the popup + /// + public void OpenAddParameterPopup() + { + this.AddParameterViewModel.ResetValues(); + this.IsOnAddingParameterMode = true; + } + + /// + /// Add a new based on an existing + /// + /// The + public async Task CopyAndAddNewElement(ElementBase elementBase) + { + this.IsLoading = true; + + if (elementBase.GetContainerOfType() == this.CurrentThing) + { + var copyCreator = new CopyElementDefinitionCreator(this.sessionService.Session); + + try + { + await copyCreator.Copy((ElementDefinition)elementBase, true); + } + catch (Exception exception) + { + Console.WriteLine(exception.Message); + throw; + } + finally + { + this.IsLoading = false; + } + } + else + { + var copyCreator = new CopyCreator(this.sessionService.Session); + + try + { + await copyCreator.Copy((ElementDefinition)elementBase, this.CurrentThing); + } + catch (Exception exception) + { + Console.WriteLine(exception.Message); + throw; + } + finally + { + this.IsLoading = false; + } + } + } + + /// + /// Add a new based on an existing + /// + /// The to be added as + /// The where to add the new to + public async Task AddNewElementUsage(ElementBase fromElementBase, ElementBase toElementBase) + { + if (fromElementBase.GetContainerOfType() == this.CurrentThing && toElementBase.GetContainerOfType() == this.CurrentThing) + { + this.IsLoading = true; + + var thingCreator = new ThingCreator(); + + try + { + await thingCreator.CreateElementUsage((ElementDefinition)toElementBase, (ElementDefinition)fromElementBase, this.sessionService.Session.OpenIterations.First(x => x.Key == this.CurrentThing).Value.Item1, this.sessionService.Session); + } + catch (Exception exception) + { + Console.WriteLine(exception.Message); + throw; + } + finally + { + this.IsLoading = false; + } + } + } + + /// + /// Tries to create a new + /// + /// A + public async Task AddingElementDefinition() + { + var thingsToCreate = new List(); + + if (this.ElementDefinitionCreationViewModel.SelectedCategories.Any()) + { + this.ElementDefinitionCreationViewModel.ElementDefinition.Category = this.ElementDefinitionCreationViewModel.SelectedCategories.ToList(); + } + + this.ElementDefinitionCreationViewModel.ElementDefinition.Container = this.CurrentThing; + thingsToCreate.Add(this.ElementDefinitionCreationViewModel.ElementDefinition); + var clonedIteration = this.CurrentThing.Clone(false); + + if (this.ElementDefinitionCreationViewModel.IsTopElement) + { + clonedIteration.TopElement = this.ElementDefinitionCreationViewModel.ElementDefinition; + } + + clonedIteration.Element.Add(this.ElementDefinitionCreationViewModel.ElementDefinition); + thingsToCreate.Add(clonedIteration); + + try + { + await this.sessionService.CreateOrUpdateThings(clonedIteration, thingsToCreate); + this.IsOnCreationMode = false; + } + catch (Exception exception) + { + Console.WriteLine(exception.Message); + throw; + } + } + + /// + /// Handles the message received + /// + /// A + protected override async Task OnEndUpdate() + { + await this.OnSessionRefreshed(); + } + + /// + /// Handles the refresh of the current + /// + /// A + protected override Task OnSessionRefreshed() + { + this.SelectElement(this.SelectedElementDefinition); + return Task.CompletedTask; + } + + /// + /// Update this view model properties + /// + /// A + protected override async Task OnThingChanged() + { + await base.OnThingChanged(); + + if (this.CurrentThing == null) + { + return; + } + + this.AddParameterViewModel.InitializeViewModel(this.CurrentThing); + this.ElementDefinitionCreationViewModel.InitializeViewModel(this.CurrentThing); + + this.IsLoading = false; + } + } +} diff --git a/COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementBaseTreeRowViewModel.cs b/COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementBaseTreeRowViewModel.cs new file mode 100644 index 00000000..97ff46b8 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementBaseTreeRowViewModel.cs @@ -0,0 +1,93 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The COMET WEB Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The COMET WEB Community Edition is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMETwebapp.ViewModels.Components.MultiModelEditor.Rows +{ + using CDP4Common.EngineeringModelData; + + using COMETwebapp.ViewModels.Components.ModelEditor.Rows; + + using ReactiveUI; + + /// + /// Row View Model for + /// + public abstract class ElementBaseTreeRowViewModel : ReactiveObject + { + /// + /// Backing field for + /// + private ElementBase elementBase; + + /// + /// Backing field for + /// + private string elementName; + + /// + /// Initializes a new instance of the class. + /// the + /// + protected ElementBaseTreeRowViewModel(ElementBase elementBase) + { + this.ElementBase = elementBase; + this.ElementName = elementBase.Name; + } + + /// + /// Initializes a new instance of the class. + /// + protected ElementBaseTreeRowViewModel() + { + } + + /// + /// The name of the + /// + public string ElementName + { + get => this.elementName; + set => this.RaiseAndSetIfChanged(ref this.elementName, value); + } + + /// + /// The + /// + public ElementBase ElementBase + { + get => this.elementBase; + set => this.RaiseAndSetIfChanged(ref this.elementBase, value); + } + + /// + /// Update this row view model properties + /// + /// The to use for updating + public void UpdateProperties(ElementBaseTreeRowViewModel elementBaseTreeRow) + { + this.ElementBase = elementBaseTreeRow.elementBase; + this.ElementName = elementBaseTreeRow.elementName; + } + } +} diff --git a/COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementDefinitionTreeTreeRowViewModel.cs b/COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementDefinitionTreeTreeRowViewModel.cs new file mode 100644 index 00000000..6d0d62bf --- /dev/null +++ b/COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementDefinitionTreeTreeRowViewModel.cs @@ -0,0 +1,103 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The COMET WEB Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The COMET WEB Community Edition is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMETwebapp.ViewModels.Components.MultiModelEditor.Rows +{ + using System.Collections.ObjectModel; + + using CDP4Common.EngineeringModelData; + + using COMETwebapp.ViewModels.Components.ModelEditor.Rows; + + using DynamicData; + + using ReactiveUI; + + /// + /// Row View Model for + /// + public class ElementDefinitionTreeTreeRowViewModel : ElementBaseTreeRowViewModel + { + /// + /// Backing field for + /// + private bool isTopElement; + + /// + /// Gets or the collection of + /// + public ObservableCollection Rows { get; } = new(); + + /// + /// Initializes a new instance of the class. + /// the + /// + public ElementDefinitionTreeTreeRowViewModel(ElementDefinition elementBase) : base(elementBase) + { + this.IsTopElement = elementBase == elementBase.GetContainerOfType().TopElement; + + var elementUsages = elementBase?.ContainedElement; + + if (elementUsages?.Any() ?? false) + { + this.Rows.AddRange(elementUsages.Select(x => new ElementUsageTreeTreeRowViewModel(x))); + } + } + + /// + /// Initializes a new instance of the class. + /// + public ElementDefinitionTreeTreeRowViewModel() + { + } + + /// + /// The value to check if the element base is the top element + /// + public bool IsTopElement + { + get => this.isTopElement; + set => this.RaiseAndSetIfChanged(ref this.isTopElement, value); + } + + /// + /// Update this row view model properties + /// + /// The to use for updating + public void UpdateProperties(ElementDefinitionTreeTreeRowViewModel elementDefinitionTreeRow) + { + base.UpdateProperties(elementDefinitionTreeRow); + this.IsTopElement = elementDefinitionTreeRow.isTopElement; + + this.Rows.Clear(); + + var elementUsages = (elementDefinitionTreeRow.ElementBase as ElementDefinition)?.ContainedElement; + + if (elementUsages != null) + { + this.Rows.AddRange(elementUsages.Select(x => new ElementUsageTreeTreeRowViewModel(x))); + } + } + } +} diff --git a/COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementUsageTreeTreeRowViewModel.cs b/COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementUsageTreeTreeRowViewModel.cs new file mode 100644 index 00000000..bb489957 --- /dev/null +++ b/COMETwebapp/ViewModels/Components/MultiModelEditor/Rows/ElementUsageTreeTreeRowViewModel.cs @@ -0,0 +1,60 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2024 Starion Group S.A. +// +// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua +// +// This file is part of COMET WEB Community Edition +// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The COMET WEB Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The COMET WEB Community Edition is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace COMETwebapp.ViewModels.Components.MultiModelEditor.Rows +{ + using CDP4Common.EngineeringModelData; + + using COMETwebapp.ViewModels.Components.ModelEditor.Rows; + + /// + /// Row View Model for + /// + public class ElementUsageTreeTreeRowViewModel : ElementBaseTreeRowViewModel + { + /// + /// Initializes a new instance of the class. + /// the + /// + public ElementUsageTreeTreeRowViewModel(ElementUsage elementUsage) : base(elementUsage) + { + } + + /// + /// Initializes a new instance of the class. + /// + public ElementUsageTreeTreeRowViewModel() + { + } + + /// + /// Update this row view model properties + /// + /// The to use for updating + public void UpdateProperties(ElementUsageTreeTreeRowViewModel elementUsageTreeRow) + { + base.UpdateProperties(elementUsageTreeRow); + } + } +}