Ticket T1176605
Visible to All Users

XAF - Entity Framework Core (EF Core) version of the Clone Object Module

created 2 years ago

As described in Ticket T1131164, DevExpress does not see the need of implementing Clone features in EF Core due to low demands. But, since EF now seems to be the preferred ORM architecture the situation has changed. I think, the ability of copying (or cloning) of objects and their substructures is urgently needed, to create new productive objects based on repository objects (and their substructures). The suggested workaround in Ticket T1131164 unfortunately seems only to be working for a single record without dependent structures.

Is there any other suggestion?

Regards,
Haiko

Comments (1)
MB MB
Mario Blatarić 2 years ago

    Hi,

    No problem, I am sure you will have additional work :-)

    For instance, you can expand Kloniraj function and use EF Core to go through all navigation properties, then use reflection to check for Aggregate attribute on those properties and clone those sub-objects automatically - if that is your common scenario.

    I did not implement this, since I it is easier for me to handle those cases manually (because when I need to clone dependent objects I always have to do additional processing) than clear sub-objects for all other classes.

    Regards,
    Mario

    Answers approved by DevExpress Support

    created 7 days ago

    XAF v25.1 WinForms/Blazor EFCore-based apps will support the Clone command (to quickly copy data records, including aggregated detail collections. We unified cloning functionality and APIs for both ORMs, so XAF developers can use a single DevExpress.ExpressApp.CloneObject package.
    No breaking changes are expected for existing XPO-based projects when using the DevExpress Project Converter (to handle all naming-related changes automatically). See Also: T1276850 - Core - The DevExpress.ExpressApp.CloneObject.Xpo assembly and NuGet package have been renamed.

    XAF v25.1 is set for release in June 2025, early access preview (EAP) will likely be available at the end of March 2025.

    Let us know if you have any questions or suggestions about this feature.

      created 2 years ago (modified 2 years ago)

      Hi,

      I implemented my own solution for EF Core based on XPO version of CloneObject module.

      Currently, I am a bit tight with free time, so I will just post as is with few comments.
      Keep in mind, this was developed for my specific scenarios, however it can be expanded to cover different scenarios as well.

      Here is the new controller:

      C#
      using DevExpress.ExpressApp; using DevExpress.ExpressApp.Actions; using DevExpress.ExpressApp.ApplicationBuilder; using DevExpress.ExpressApp.DC; using DevExpress.ExpressApp.EFCore; using DevExpress.ExpressApp.Model; using DevExpress.ExpressApp.Security; using DevExpress.ExpressApp.SystemModule; using DevExpress.ExpressApp.Utils; using DevExpress.Persistent.Base; using DevExpress.XtraSpreadsheet.Model; using Link5.Data; using Microsoft.EntityFrameworkCore; using System.ComponentModel; namespace Link5.Module.Common.Controllers.ObjectCloner; internal class LgnObjectClone { } public class LgnCloneObjectViewController : ObjectViewController { public const string CloneObjectActionId = "CloneObject"; public const string CloneObjectActionImageName = "Action_CloneMerge_Clone_Object"; public const string IsNotModifiedEnabledKey = "NotModified"; private const string AllowNewActiveKey = "AllowNew"; private SingleChoiceAction cloneObjectAction; private bool allowCloneWhenModified = false; private Type clonerType; public Type ClonerType { get { return clonerType; } set { clonerType = value; } } private void SynchActionWithViewReadOnly() { foreach (ChoiceActionItem item in CloneObjectAction.Items) { if (View.IsRoot) { if (View.ObjectTypeInfo.Type.IsAssignableFrom((Type)item.Data)) { item.Enabled["ViewIsNotReadOnly"] = View.AllowNew; } } else { item.Enabled["ViewIsNotReadOnly"] = View.AllowNew; } } } private void CloneObjectAction_OnExecute(object sender, SingleChoiceActionExecuteEventArgs args) { CloneObject(args); } private void View_AllowNewChanged(object sender, EventArgs e) { UpdateActionsState(); } private void ObjectSpace_ModifiedChanged(object sender, EventArgs e) { UpdateActionsState(); } private void CollectionSource_CollectionChanged(object sender, EventArgs e) { UpdateActionsState(); } private void NewObjectViewController_Activated(object sender, EventArgs e) { UpdateActionsState(); } private bool IsRootDetailView(IObjectSpace objectSpace) { if (this.View.ObjectSpace == objectSpace) { return false; } return true; } protected virtual void UpdateActionsState() { SynchActionWithViewReadOnly(); cloneObjectAction.Active.SetItemValue(AllowNewActiveKey, View.AllowNew); cloneObjectAction.Enabled[IsNotModifiedEnabledKey] = allowCloneWhenModified || !View.ObjectSpace.IsModified; bool isNestedDetailView = (View is DetailView) && !View.IsRoot; cloneObjectAction.Active.SetItemValue("IsRootDetailView", !isNestedDetailView); cloneObjectAction.Active["PopupWindowContext"] = Frame.Context != TemplateContext.PopupWindow; var newObjectViewController = Frame.GetController<NewObjectViewController>(); if (newObjectViewController != null && newObjectViewController.Active) { newObjectViewController.UpdateIsManyToManyKey(cloneObjectAction); newObjectViewController.SetSecurityAllowNewByPermissions(cloneObjectAction); } } protected virtual void CloneObject(SingleChoiceActionExecuteEventArgs args) { if (args.SelectedChoiceActionItem != null) { object currentObject = View.ObjectSpace.GetObject(View.CurrentObject); Type targetType = (Type)args.SelectedChoiceActionItem.Data; CloneObject(currentObject, targetType, out IObjectSpace objectSpace, out object clonedObj); if (clonedObj != null) { ShowClonedObject(args, currentObject, objectSpace, clonedObj); } else { throw new Exception("Kloniranje nije uspjelo!"); } } } protected void CloneObject(object currentObject, Type targetType, out IObjectSpace targetObjectSpace, out object clonedObj) { Guard.ArgumentNotNull(currentObject, "currentObject"); LgnCustomCloneObjectEventArgs cloneObjectArgs = new LgnCustomCloneObjectEventArgs(currentObject, targetType, Application, Frame, View); OnCustomCloneObject(cloneObjectArgs); if (cloneObjectArgs.ClonedObject != null) { Guard.ArgumentNotNull(cloneObjectArgs.TargetObjectSpace, "cloneObjectArgs.TargetObjectSpace"); clonedObj = cloneObjectArgs.ClonedObject; targetObjectSpace = cloneObjectArgs.TargetObjectSpace; } else { if (cloneObjectArgs.TargetObjectSpace == null) { cloneObjectArgs.TargetObjectSpace = cloneObjectArgs.CreateDefaultTargetObjectSpace(); } targetObjectSpace = cloneObjectArgs.TargetObjectSpace; Guard.ArgumentNotNull(targetObjectSpace, "targetObjectSpace"); object objectFromTargetObjectSpace = targetObjectSpace.GetObject(currentObject); if (objectFromTargetObjectSpace == null) { Guard.ArgumentNotNull(objectFromTargetObjectSpace, "objectFromTargetObjectSpace"); } var cloner = new LgnCloner(); clonedObj = cloner.Kloniraj(targetObjectSpace, objectFromTargetObjectSpace, targetType); LgnCustomizeClonedObjectEventArgs customizeClonedObjArgs = new(currentObject, clonedObj, targetObjectSpace, targetType); OnCustomizeClonedObject(customizeClonedObjArgs); } } protected void ShowClonedObject(SingleChoiceActionExecuteEventArgs args, object currentObject, IObjectSpace objectSpace, object clonedObj) { LgnCustomShowClonedObjectEventArgs showClonedObjectArgs = new LgnCustomShowClonedObjectEventArgs(objectSpace, currentObject, clonedObj, args.ShowViewParameters); OnCustomShowClonedObject(showClonedObjectArgs); if (!showClonedObjectArgs.Handled) { string viewId = Application.FindDetailViewId(currentObject.GetType()); if (!string.IsNullOrEmpty(viewId) && IsRootDetailView(objectSpace)) { objectSpace.SetModified(clonedObj); args.ShowViewParameters.CreatedView = Application.CreateDetailView(objectSpace, clonedObj, View); } else { if ((View is ListView) && View.ObjectSpace == objectSpace) { ((ListView)View).CollectionSource.Add(clonedObj); } else { throw new InvalidOperationException(string.Format(@"Cannot find the default Detail View for the '{0}' type.", currentObject.GetType())); } } } } protected virtual void OnCustomCloneObject(LgnCustomCloneObjectEventArgs args) { if (CustomCloneObject != null) { CustomCloneObject(this, args); } } protected virtual void OnCustomizeClonedObject(LgnCustomizeClonedObjectEventArgs args) { if (CustomizeClonedObject != null) { CustomizeClonedObject(this, args); } } protected virtual void OnCustomShowClonedObject(LgnCustomShowClonedObjectEventArgs showClonedObjectArgs) { if (CustomShowClonedObject != null) { CustomShowClonedObject(this, showClonedObjectArgs); } } public LgnCloneObjectViewController() { TypeOfView = typeof(ObjectView); this.cloneObjectAction = new DevExpress.ExpressApp.Actions.SingleChoiceAction(this, CloneObjectActionId, PredefinedCategory.ObjectsCreation); this.cloneObjectAction.Caption = "Dupliciraj"; this.cloneObjectAction.ToolTip = "Dupliciraj objekt"; this.cloneObjectAction.ImageName = CloneObjectActionImageName; this.cloneObjectAction.SelectionDependencyType = DevExpress.ExpressApp.Actions.SelectionDependencyType.RequireSingleObject; this.cloneObjectAction.ItemType = DevExpress.ExpressApp.Actions.SingleChoiceActionItemType.ItemIsOperation; this.cloneObjectAction.Execute += new DevExpress.ExpressApp.Actions.SingleChoiceActionExecuteEventHandler(this.CloneObjectAction_OnExecute); } protected override void OnActivated() { base.OnActivated(); CloneObjectAction.BeginUpdate(); try { CloneObjectAction.Items.Clear(); Dictionary<IModelNode, Type> targetTypes = GetCloneActionTargetTypes(((ObjectView)View).ObjectTypeInfo); foreach (IModelNode node in targetTypes.Keys) { IModelClass modelClass = (IModelClass)node; ChoiceActionItem item = new ChoiceActionItem(modelClass.Name, modelClass.Caption, targetTypes[node]); item.ImageName = modelClass.ImageName; CloneObjectAction.Items.Add(item); string diagnosticInfo = ""; if (!DataManipulationRight.CanCreate(null, targetTypes[node], null, out diagnosticInfo)) { item.Active.SetItemValue("Security", false); } } } finally { CloneObjectAction.EndUpdate(); } UpdateActionsState(); View.AllowNewChanged += new EventHandler(View_AllowNewChanged); View.ObjectSpace.ModifiedChanged += new EventHandler(ObjectSpace_ModifiedChanged); var listView = View as ListView; if (listView != null) { if (listView.CollectionSource is PropertyCollectionSource) { PropertyCollectionSource collectionSource = (PropertyCollectionSource)listView.CollectionSource; UpdateActionState(collectionSource); collectionSource.MasterObjectChanged += collectionSource_MasterObjectChanged; } listView.CollectionSource.CollectionChanged += CollectionSource_CollectionChanged; } var newObjectViewController = Frame.GetController<NewObjectViewController>(); if (newObjectViewController != null) { newObjectViewController.Activated += NewObjectViewController_Activated; } } void collectionSource_MasterObjectChanged(object sender, EventArgs e) { UpdateActionState((PropertyCollectionSource)sender); } private void UpdateActionState(PropertyCollectionSource collectionSource) { if (SecuritySystem.Instance != null && SecuritySystem.Instance is IRequestSecurity && collectionSource.MasterObject != null) { CloneObjectAction.Enabled["IsGranted"] = SecuritySystem.IsGranted(new PermissionRequest(collectionSource.ObjectSpace, collectionSource.MasterObjectType, SecurityOperations.Write, collectionSource.MasterObject, collectionSource.MemberInfo.Name)); } else { CloneObjectAction.Enabled.RemoveItem("IsGranted"); } } private Dictionary<IModelNode, Type> GetCloneActionTargetTypes(ITypeInfo sourceType) { LgnCustomGetCloneActionTargetTypesEventArgs args = new LgnCustomGetCloneActionTargetTypesEventArgs(sourceType, GetApplicationModel()); if (CustomGetCloneActionTargetTypes != null) { CustomGetCloneActionTargetTypes(this, args); if (args.Handled) { return args.TargetTypes; } } return args.GetDefaultTargetTypes(); } private IModelApplication GetApplicationModel() { return (Application != null) ? Application.Model : null; } protected override void OnDeactivated() { View.AllowNewChanged -= new EventHandler(View_AllowNewChanged); View.ObjectSpace.ModifiedChanged -= new EventHandler(ObjectSpace_ModifiedChanged); var listView = View as ListView; if (listView != null) { if (listView.CollectionSource is PropertyCollectionSource) { ((PropertyCollectionSource)listView.CollectionSource).MasterObjectChanged -= collectionSource_MasterObjectChanged; } listView.CollectionSource.CollectionChanged -= CollectionSource_CollectionChanged; } var newObjectViewController = Frame.GetController<NewObjectViewController>(); if (newObjectViewController != null) { newObjectViewController.Activated -= NewObjectViewController_Activated; } base.OnDeactivated(); } [DefaultValue(false)] public bool AllowCloneWhenModified { get { return allowCloneWhenModified; } set { allowCloneWhenModified = value; } } public SingleChoiceAction CloneObjectAction { get { return cloneObjectAction; } } public event EventHandler<LgnCustomCloneObjectEventArgs> CustomCloneObject; public event EventHandler<LgnCustomizeClonedObjectEventArgs> CustomizeClonedObject; public event EventHandler<LgnCustomShowClonedObjectEventArgs> CustomShowClonedObject; public event EventHandler<LgnCustomGetCloneActionTargetTypesEventArgs> CustomGetCloneActionTargetTypes; } public class LgnCustomGetCloneActionTargetTypesEventArgs : HandledEventArgs { private IModelApplication applicationModel; private ITypeInfo sourceType; private Dictionary<IModelNode, Type> targetTypes = new Dictionary<IModelNode, Type>(); public LgnCustomGetCloneActionTargetTypesEventArgs(ITypeInfo sourceType, IModelApplication applicationModel) { Guard.ArgumentNotNull(sourceType, "sourceType"); this.sourceType = sourceType; this.applicationModel = applicationModel; } public Dictionary<IModelNode, Type> GetDefaultTargetTypes() { Dictionary<IModelNode, Type> result = new Dictionary<IModelNode, Type>(); if (applicationModel != null) { LgnCloneObjectActionHelper helper = new LgnCloneObjectActionHelper(applicationModel); IList<Type> targetTypes = helper.GetTargetTypes(sourceType.Type); foreach (Type destinationType in targetTypes) { IModelClass modelClass = (IModelClass)applicationModel.BOModel.GetClass(destinationType); if (modelClass != null) { if (destinationType.IsInterface) { IDCEntityStore dcEntityStore = (IDCEntityStore)((TypesInfo)XafTypesInfo.Instance).FindEntityStore(typeof(IDCEntityStore)); Type generatedEntityType = dcEntityStore != null ? dcEntityStore.GetGeneratedEntityType(destinationType) : null; if (generatedEntityType != null) { result.Add(modelClass, generatedEntityType); } } else { result.Add(modelClass, destinationType); } } } } return result; } public ITypeInfo SourceType => sourceType; public Dictionary<IModelNode, Type> TargetTypes => targetTypes; } public class LgnCustomCloneObjectEventArgs : EventArgs { private XafApplication application; private Frame frame; private View view; public LgnCustomCloneObjectEventArgs(object sourceObject, Type targetType, XafApplication application, Frame frame, View view) { this.SourceObject = sourceObject; this.TargetType = targetType; this.application = application; this.frame = frame; this.view = view; } public IObjectSpace TargetObjectSpace { get; set; } public IObjectSpace CreateDefaultTargetObjectSpace() { if (application.ShowDetailViewFrom(frame) || (view.ObjectSpace.Owner == view)) { return application.GetObjectSpaceToShowDetailViewFrom(frame, TargetType); } else { return view.ObjectSpace; } } public object SourceObject { get; private set; } public object ClonedObject { get; set; } public Type TargetType { get; private set; } } public class LgnCustomizeClonedObjectEventArgs : EventArgs { public LgnCustomizeClonedObjectEventArgs(object sourceObject, object clonedObject, IObjectSpace targetObjectSpace,Type targetType) { SourceObject = sourceObject; ClonedObject = clonedObject; TargetObjectSpace = targetObjectSpace; TargetType = targetType; } public object SourceObject { get; set; } public object ClonedObject { get; set; } public IObjectSpace TargetObjectSpace { get; set; } public Type TargetType { get; private set; } } public class LgnCustomShowClonedObjectEventArgs : HandledEventArgs { private object sourceObject; private object clonedObject; private IObjectSpace targetObjectSpace; public LgnCustomShowClonedObjectEventArgs(IObjectSpace targetObjectSpace, object sourceObject, object clonedObject, ShowViewParameters showViewParameters) { this.targetObjectSpace = targetObjectSpace; this.sourceObject = sourceObject; this.clonedObject = clonedObject; this.ShowViewParameters = showViewParameters; } public IObjectSpace TargetObjectSpace => targetObjectSpace; public object SourceObject => sourceObject; public object ClonedObject => clonedObject; public ShowViewParameters ShowViewParameters { get; private set; } } public class LgnCloneObjectActionHelper { private IModelApplication application; private bool IsCloneable(Type type) { IModelClassCloneable modelClass = (IModelClassCloneable)application.BOModel.GetClass(type); return modelClass != null ? modelClass.IsCloneable : false; } private bool IsCreatable(Type type) { if (type.IsClass && !type.IsAbstract) { return true; } IDCEntityStore dcEntityStore = (IDCEntityStore)((TypesInfo)XafTypesInfo.Instance).FindEntityStore(typeof(IDCEntityStore)); return type.IsInterface && dcEntityStore != null && dcEntityStore.GetGeneratedEntityType(type) != null; } public IList<Type> GetTargetTypes(Type sourceType) { if (IsCloneable(sourceType)) { Type rootClonedType = sourceType; Type currentType = sourceType.BaseType; while (currentType != null) { if (IsCloneable(currentType)) { rootClonedType = currentType; } currentType = currentType.BaseType; } List<Type> result = GetDescendantTargetTypes(XafTypesInfo.Instance.FindTypeInfo(rootClonedType), sourceType); return result; } else { return Type.EmptyTypes; } } private List<Type> GetDescendantTargetTypes(ITypeInfo currentTypeInfo, Type sourceType) { List<Type> result = new List<Type>(); foreach (ITypeInfo typeInfo in currentTypeInfo.Descendants) { if (IsCloneable(typeInfo.Type) && IsCreatable(typeInfo.Type)) { result.Add(typeInfo.Type); } } result.Remove(sourceType); if (IsCreatable(sourceType)) { result.Insert(0, sourceType); } return result; } public LgnCloneObjectActionHelper(IModelApplication application) { this.application = application; } } public class LgnCloner { private Dictionary<string, object> Values = new(); public object Kloniraj(IObjectSpace targetObjectSpace, object sourceObject, Type targetType) { if (targetObjectSpace is EFCoreObjectSpace efOS) { if (efOS.DbContext is Link5DbContext ctx) { Values.Clear(); ctx.Entry(sourceObject).Reload(); var values = ctx.Entry(sourceObject).CurrentValues.Clone(); foreach (var propName in values.Properties.Where(x => !x.IsPrimaryKey())) { Values.Add(propName.Name, values[propName.Name]); } var targetObject = efOS.CreateObject(targetType); ctx.Entry(targetObject).CurrentValues.SetValues(Values); return targetObject; } } return null; } }

      This is mostly DevExpress original code, I renamed classes just so I do not have to worry about any clashing now or in the future.
      I also introduced new event CustomizeClonedObject which is more preferred in my case than entirely custom clone procedure.

      I use this custom event to clone dependent structures when I need to, because I do not need to in most cases, however, you could expand Kloniraj method (Kloniraj is just Croatian version of Clone word) to handle dependant structures automatically if that is your preference.

      Here is an example how I do it:

      C#
      public class Hodogram_Controller : LgnBOView_Controller<Hodogram> { protected override void OnActivated() { base.OnActivated(); if (cloneController != null) { cloneController.CustomizeClonedObject += CustomizeClonedObject; } } protected override void OnDeactivated() { if (cloneController != null) { cloneController.CustomizeClonedObject -= CustomizeClonedObject; } base.OnDeactivated(); } private void CustomizeClonedObject(object sender, LgnCustomizeClonedObjectEventArgs e) { if (e.SourceObject is Hodogram src && e.ClonedObject is Hodogram dst) { var cloner = new LgnCloner(); foreach (var s in src.StavkeHodograma.ToArray()) { var ss = cloner.Kloniraj(e.TargetObjectSpace, s, typeof(HodogramStavka)) as HodogramStavka; dst.StavkeHodograma.Add(ss); } } } }

      All controllers are assigned in base LgnBOView_Controller which is why you do not see Frame.GetController here.

      Also, in order to get IsCloneable in model, you need this interface:

      C#
      public interface IModelClassCloneable { [ModelValueCalculator("((IModelClassCloneable)BaseClass)", "IsCloneable")] [Description("Indicates whether objects of the current class can be cloned."), Category("Behavior")] bool IsCloneable { get; set; } }

      And, you need to override a method in your Module.cs to register that interface:

      C#
      public override void ExtendModelInterfaces(ModelInterfaceExtenders extenders) { extenders.Add<IModelClass, IModelClassCloneable>(); base.ExtendModelInterfaces(extenders); }

      This will get you a Clone action in all views for business classes in which you enabled IsCloneable, same like in original controller.

      I just copied code as-is, so it could be there are some extension methods or something.
      Anyway, ask away if you have issues implementing it.

      Regards,
      Mario

        Show previous comments (1)
        Dennis Garavsky (DevExpress) 2 years ago

          Thank you for sharing your solution with the developer community, Mario.

          HB HB
          Haiko Buchholz 1 2 years ago

            Is there any implementation of "LgnBOView_Controller" available? I think, cloneControler is defined inside.

            Thank you,
            Haiko

            MB MB
            Mario Blatarić 2 years ago

              Hi,

              LgnBOView_Controller is not important in this case, cloneController is just a field of type LgnCloneObjectViewController defined in LgnBOView_Controller (which is my base controller for all other view controllers).

              You can either define your own base controller or just use intellisense to add cloneController as field in your current controller.

              So, all the code in LgnBOView_Controller is this:

              C#
              protected LgnCloneObjectViewController cloneController;

              That's it.

              Regards,
              Mario

              Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

              Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.