Dataverse: Make use of INotifyPropertyChanged to get the latest state of Entity

Before we continue, this blog post will only work if you using Early-Bound on your plugin project (for Late-Bound, you can follow this blog post). The scenario that we will cover is to get rid of the hassle of the below method (I will show you using the code to make it easier):

var target = (Entity)Context.PluginExecutionContext.InputParameters["Target"];
var preImage = (Entity)Context.PluginExecutionContext.PreEntityImages["Image"];
var qty = target.GetAttributeValue<int?>("dev_qty") ?? 
		  preImage.GetAttributeValue<int?>("dev_qty");
// continue...

As you can see, we always need to get an attribute’s current value. But because the Target object only holds the changes that happened in UI (and also if there is an update in plugin depth < 30), we sometimes depend on the PreEntityImages or retrieve the latest data from the database (via IOrganizationService.Retrieve). Hence the above code will be used to get the latest value.

The interesting part is when we are using Early-Bound generator classes like Albanian Early Bound (or others), the entity generated will also implement the interface of INotifyPropertyChanged which enables us to register a custom method that can be called once the attribute’s value changed.

Here is the sample of the entity class that implements INotifyPropertyChanged:

[System.Runtime.Serialization.DataContractAttribute()]
[Microsoft.Xrm.Sdk.Client.EntityLogicalNameAttribute("contact")]
[System.CodeDom.Compiler.GeneratedCodeAttribute("CrmSvcUtil", "9.1.0.118")]
public partial class Contact : Microsoft.Xrm.Sdk.Entity, System.ComponentModel.INotifyPropertyChanging, System.ComponentModel.INotifyPropertyChanged
{
	
	public Contact() : 
			base(EntityLogicalName)
	{
	}
	
	public const string EntityLogicalName = "contact";
	
	public const string EntityLogicalCollectionName = "contacts";
	
	public const string EntitySetName = "contacts";
	
	public const int EntityTypeCode = 2;
	
	public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
	
	public event System.ComponentModel.PropertyChangingEventHandler PropertyChanging;
	
	private void OnPropertyChanged(string propertyName)
	{
		if ((this.PropertyChanged != null))
		{
			this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
		}
	}
	
	private void OnPropertyChanging(string propertyName)
	{
		if ((this.PropertyChanging != null))
		{
			this.PropertyChanging(this, new System.ComponentModel.PropertyChangingEventArgs(propertyName));
		}
	}
	
	[Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("accountid")]
	public Microsoft.Xrm.Sdk.EntityReference AccountId
	{
		get
		{
			return this.GetAttributeValue<Microsoft.Xrm.Sdk.EntityReference>("accountid");
		}
	}
	
	[Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("accountrolecode")]
	public System.Nullable<OptionSets.AccountRoleCode> AccountRoleCode
	{
		get
		{
			Microsoft.Xrm.Sdk.OptionSetValue attributeValue = this.GetAttributeValue<Microsoft.Xrm.Sdk.OptionSetValue>("accountrolecode");
			if ((attributeValue != null))
			{
				return ((OptionSets.AccountRoleCode)(System.Enum.ToObject(typeof(OptionSets.AccountRoleCode), attributeValue.Value)));
			}
			else
			{
				return null;
			}
		}
		set
		{
			this.OnPropertyChanging("AccountRoleCode");
			if ((value == null))
			{
				this.SetAttributeValue("accountrolecode", value);
			}
			else
			{
				this.SetAttributeValue("accountrolecode", new Microsoft.Xrm.Sdk.OptionSetValue(((int)(value))));
			}
			this.OnPropertyChanged("AccountRoleCode");
		}
	}
	
	// cut for simplify purpose 🙂
}

With the above capabilities (adding a custom method via PropertyChanged event), we can design the below workflow to get the latest value of the object:

Flow of the code to set latest object using PropertyOnChanged

From the above diagram, you can see that if the Entity implements INotifyPropertyChanged, we can register the event to update the Latest attribute value as well. With this, the operation will be much simpler.

The Code

You can see the full code from this GitHub repo. I only will highlight the code that is important and hopes you can understand it. For the implementation, I created these two classes (one is for Late-Bound and the generic one will handle the Early-Bound):

using System.ComponentModel;
using System.Linq;
using Lib.Extensions;
using Microsoft.Xrm.Sdk;

namespace Lib.Core
{
    public class EntityWrapper : IEntityWrapper<Entity>
    {
        public EntityWrapper(Entity target, Entity initial)
        {
            Initial = target;
            Target = initial;
        }

        public Entity Initial { get; }
        public Entity Target { get; }

        public Entity Latest => Get(Initial, Target);

        protected Entity Get(Entity initial, Entity target)
        {
            var entity = initial.Clone();
            foreach (var targetAttribute in target.Attributes)
            {
                entity[targetAttribute.Key] = targetAttribute.Value;
            }

            return entity;
        }
    }

    public class EntityWrapper<TEntity> : EntityWrapper, IEntityWrapper<TEntity>
        where TEntity : Entity
    {
        public EntityWrapper(TEntity target, TEntity initial) : base(target, initial)
        {
            IsAssignableNotifyPropertyChanged =
                typeof(TEntity).GetInterfaces().Any(x => x == typeof(INotifyPropertyChanged));

            if (!IsAssignableNotifyPropertyChanged)
            {
                Initial = initial;
                Target = target;
                return;
            }

            var targetWithPropertyChanged = (INotifyPropertyChanged)target;
            targetWithPropertyChanged.PropertyChanged += LatestPropertyChanged_PropertyChanged;
            Initial = initial.ToEntity<TEntity>();
            Target = targetWithPropertyChanged as TEntity;
            _latest = Get(Initial, Target).ToEntity<TEntity>();
        }

        private void LatestPropertyChanged_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            Latest[e.PropertyName.ToLower()] = ((TEntity)sender)[e.PropertyName.ToLower()];
        }

        public bool IsAssignableNotifyPropertyChanged { get; }

        public new TEntity Initial { get; }

        public new TEntity Target { get; }

        private readonly TEntity _latest;

        public new TEntity Latest
        {
            get
            {
                if (IsAssignableNotifyPropertyChanged)
                {
                    return _latest;
                }

                return Get(Initial, Target) as TEntity;
            }
        }
    }
}

As you can see from the above code, if the entity inherits from INotifyPropertyChanged, then we will register LatestPropertyChanged_PropertyChanged method. This method will ensure the _latest object will be updated to the correct value.

To use the EntityWrapper class, I created a business base class that ensures this creation will be handled 1 time. Including the setup to get the two objects (Target and PreEntityImages/needs to retrieve from the database via IOrganizationService.Retrieve):

using System;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace Lib.Core
{
    public abstract class BusinessBase<TEntity>
        where TEntity : Entity
    {
        public ILocalPluginContext Context { get; }
        private IEntityWrapper<TEntity> _wrapper;

        public IEntityWrapper<TEntity> Wrapper
        {
            get
            {
                if (_wrapper != null) return _wrapper;

                var target = Context.GetTarget<TEntity>();
                var initial = GetInitial(target.LogicalName, target.Id);
                _wrapper = new EntityWrapper<TEntity>(target, initial);

                return _wrapper;
            }
        }

        public IOrganizationService Service => Context.InitiatingUserService;

        protected BusinessBase(ILocalPluginContext context)
        {
            Context = context;
        }

        private TEntity GetInitial(string logicalName, Guid id)
        {
            var initial =
                (Context.PluginExecutionContext.Stage == 40 && Context.PluginExecutionContext.PostEntityImages.Any()
                    ? Context.PluginExecutionContext.PostEntityImages.FirstOrDefault().Value
                    : Context.PluginExecutionContext.Stage < 30 &&
                      Context.PluginExecutionContext.PreEntityImages.Any()
                        ? Context.PluginExecutionContext.PreEntityImages.FirstOrDefault().Value
                        : null) ??
                (Context.PluginExecutionContext.Stage < 30 && Context.PluginExecutionContext.MessageName == "Create" ? 
                    (Entity)Context.PluginExecutionContext.InputParameters["Target"] :
                    Service.Retrieve(logicalName, id, new ColumnSet(true)));

            return initial.ToEntity<TEntity>();
        }

        public virtual void Execute()
        {
            HandleExecute();
        }

        public abstract void HandleExecute();
    }
}

The GetInitial method will scan if the object needs to be taken from PreEntityImages or needs to be retrieved from the database (as the code will be generic, I also implement PostEntityImages). Once we got the Initial and the Target, we can create the instance of EntityWrapper class.

Last, but not least, here is the business logic that I want to implement:

using Lib.Core;
using Lib.Entities;
using Microsoft.Xrm.Sdk;

namespace Lib.Features
{
    public class SetTotal : BusinessBase<dev_Calculation>
    {
        public SetTotal(ILocalPluginContext context) : base(context)
        {
        }

        public override void HandleExecute()
        {
            var total = Wrapper.Latest.dev_Qty.GetValueOrDefault() *
                        (Wrapper.Latest.dev_PricePerUnit?.Value ?? 0) -
                        (Wrapper.Latest.dev_Discount?.Value ?? 0);
            Wrapper.Target.dev_Total = new Money(total);
        }
    }
}

Here is the demo (I register the plugin on the update):

Demo

Check out the full code here.

Happy CRM-ing!

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.