Recording object’s history of actions

Providing users with an object’s history of actions is often required for business purposes. The history may include different actions with the object and/or with related objects.

Important!
You need to decide what information you want to show in the history before implementing it because it will be rather difficult to make any changes later on. Also, you must understand that the history is stored in the database. Thus, avoid recording such frequent operations as object views (for that, for example, you can add a separate entity/register and show this information in a report).

Configuring object´s settings

First, you need to enable history recording for an object. To do that, go to ELMA Designer – Objects tab – Additional tab and enable the Save change history option:

Then you need to configure the object’s actions that you want to track in the history:

 

When you add a new action, you can also attach an icon – it will be used for displaying the action history in the web application.

History Lifecycle

The image below describes how the history is recorded in ELMA:

Below you can see the data model used for storing the history in the database (IEntityActionHistory)

How to save object history items (events)

Once the object is configured, you need to process and save history events to the database. You can do that using the EleWise.ELMA.Events.Audit.IEntityActionEventAggregator extension point.

/// <summary>
/// Extension point for aggregation of object events 
/// </summary>
[ExtensionPoint(ServiceScope.Shell)]
public interface IEntityActionEventAggregator
{
    /// <summary>
    /// Aggregate events in the list (delete duplicates, unite common events)
    /// </summary>
    /// <param name="eventList">List of current events within transaction</param>
    /// <param name="previousResults">List of results of the previous aggregators </param>
    /// <returns>Execution results</returns>
    IEnumerable<ActionEventAggregatorResult> Aggregate(IList<EntityActionEventArgs> eventList, IEnumerable<ActionEventAggregatorResult> previousResults);
}

Data processing means preparing events (history items) of the EleWise.ELMA.Model.Events.EntityActionEventArgs type for saving (aggregation, conversion, and deletion of excessive events). You must remember that the three base events Create, Update and Delete are always saved to the database.

Saving simple events

To save simple events that do not cause changes in the object’s properties, you need to implement the IEntityActionEventAggregator extension point. For example, to record the Add comment to Event object action you can use the following code:

/// <summary>
/// Event aggregator for Calendar Event to process simple actions
/// </summary>
[Component]
internal class CalendarEventSimpleActionsEventAggregator : IEntityActionEventAggregator
{
    public IEnumerable<ActionEventAggregatorResult> Aggregate(IList<EntityActionEventArgs> eventList, IEnumerable<ActionEventAggregatorResult> previousResults)
    {
        return eventList.Where(e => e.Action.Uid == CalendarEventActions.AddCommentGuid).Select(e => new ActionEventAggregatorResult(this, e, true)).ToList();
    }
}

This code checks if the event type identifier = identifier of Add comment event (we added this action in Designer earlier).

Saving complex events

Most events change the object model. For example, the task status changes when you complete the In Work action. To process such events, use the EleWise.ELMA.Events.Audit.Impl.BaseEntityUpdateEventAggregator base class-implementation of the IEntityActionEventAggregator extension point.

In our case, to track and save the Change event time and Complete events, you can use the following code:

/// <summary>
/// Event optimizer for Calendar Event that processes actions concerning modification of fields
/// </summary>
[Component]
internal class CalendarEventBaseEditActionsEventAggregator : BaseEntityUpdateEventAggregator
{
    #region Overrides of BaseEntityUpdateEventAggregator
 
    /// <summary>
    /// List of action identifiers that must be processed by the aggregator
    /// </summary>
    protected override IEnumerable<Guid> ProcessedActions
    {
        get
        {
            yield return CalendarEventActions.ChangeTimeGuid;
            yield return CalendarEventActions.CompleteGuid;
        }
    }
 
    #endregion
}

Default base class methods do the following:

  • It checks event IDs and prevents creating events with the same IDs;
  • It also finds the information about the entity’s old state (before action happened) and adds it to the event.

How does it work?

Event in ELMA is represented by EntityActionEventArgs class. Among other information, this class contains 2 properties:

//New Entity (after change)
        public virtual IEntity New
        {
            get;
            set;
        }
        
        //Old Entity (before change)
        public virtual IEntity Old
        {
            get;
            set;
        }

Thus, the base class finds the appropriate EleWise.ELMA.Model.Actions.DefaultEntityActions.Update base event in the list of transaction’s events gets the old entity’s data and adds it to the Old property of the EntityActionEventArgs class.

If necessary, you can also override any of the base class methods.

How to implement custom history events

As mentioned above, events in ELMA are based on the EleWise.ELMA.Model.Events.EntityActionEventArgs class and its heirs. One of the heirs is the class that implements the Update entity event (EleWise.ELMA.Model.Events.EditEntityActionEventArgs). According to these classes, ELMA generates entities of EleWise.ELMA.Common.ModelsIEntityActionHistory type when events are saved to the database.

Generally, to create a custom history event, you must do the following:

  • Create a new class and inherit it from EntityActionEventArgs
  • Mark it with the EleWise.ELMA.Model.Attributes.UidAttribute attribute
  • In your class you need to override the byte[] GetAdditionalData() and SetAdditionalData(byte[] data) methods

Data returned by the GetAdditionalData method will be saved to database and then when a user requests the object history, this data will be passed to the SetAdditionalData method. Thus, if you override these methods, you must ensure Backwards Compatibility.

Generate a new GUID and pass it to the UidAttribute attribute:

[Uid("{94DBA151-F7AF-4B61-9EE1-C03CC632E804}")]
public class TaskActionEventArgs : EntityActionEventArgs
{
…
}

How to display history in the web application?

To display object history in the web application, you can use the EleWise.ELMA.Web.Mvc.Models.History.BaseAuditEventRender base class-implementation of the EleWise.ELMA.Web.Mvc.ExtensionPoints.IAuditEventRender extension point.

Define models for views

Firstly, you need to define the models that will be used in views to display the history (it is the back end component). These models must inherit the EleWise.ELMA.Common.Models.HistoryBaseModel base class:

/// <summary>
/// Model for displaying the history of calendar event for ‘Add comment’ action
/// </summary>
public class CommentCalendarEventHistoryModel : HistoryBaseModel, ICommentedHistoryModel, IAttachedHistoryModel, ICalendarEventHistoryModel
{
    /// <summary>
    /// Builder
    /// </summary>
    /// <param name="originalEvent">Event</param>
    /// <param name="actionTheme">Subject</param>
    public CommentCalendarEventHistoryModel(EntityActionEventArgs originalEvent, string actionTheme) 
        : base(originalEvent, actionTheme)
    {
    }
 
    /// <summary>
    /// Comment
    /// </summary>
    public IComment Comment { get; set; }
 
    /// <summary>
    /// List of attachments
    /// </summary>
    public ICollection<IAttachment> Attachments { get; set; }
}

Look at the interfaces that this class inherits. There is ICalendarEventHistoryModel – an empty interface from Calendar module, we will use it later to define the model class. Other interfaces inherit the EleWise.ELMA.Events.Audit.IHistoryBaseModel base interface for displaying history.

Currently, there are 3 different types of history items:

  • Comment (EleWise.ELMA.Common.Models.ICommentedHistoryModel)
  • Attachment (EleWise.ELMA.Common.Models.IAttachedHistoryModel)
  • Question \ Answer (EleWise.ELMA.Tasks.Models.IQuestionedHistoryModel) 

Later on, we will show you how to create your own history items. 

How to collect additional data for displaying in history?

To collect the data that is not part of the main history, you can use the EleWise.ELMA.Events.Audit.IEntityActionHistoryCollector extension point. This interface has only one class:

/// <summary>
/// Get data to display in history
/// </summary>
/// <param name="id">Object ID</param>
/// <param name="actionObject">ID of entity type</param>
/// <returns>List of arguments to add to the general list of items to be displayed</returns>
IEnumerable<EntityActionEventArgs> CollectHistory(long id, Guid actionObject);

Implementation of this interface must return the list of events for processing. E.g. we use the following implementation to display questions in tasks:

/// <summary>
/// Collecting additional data to show questions in a task’s history
/// </summary>
[Component]
public class TaskQuestionHistoryCollector : IEntityActionHistoryCollector
{
    public IEntityActionHistoryEventService HistoryEventService { get; set; }
 
    #region Implementation of IEntityActionHistoryCollector
 
    public IEnumerable<EntityActionEventArgs> CollectHistory(long id, Guid actionObject)
    {
        var result = new List<EntityActionEventArgs>();
        var entityMetadata = MetadataServiceContext.Service.GetMetadata(actionObject) as EntityMetadata;
        if (entityMetadata != null)
        {
            var classes = MetadataLoader.GetBaseClasses(entityMetadata);
            classes.Add(entityMetadata);
 
            if (classes.Any(c => c.Uid == InterfaceActivator.UID<ITaskBase>()))
            {
                var questions = QuestionManager.Instance.GetQuestions(id, actionObject);
 
                foreach (var question in questions)
                {
                    if (question == null) continue;
 
                    var @event = EntityActionEventArgs.TryCreate(null, question, DefaultEntityActions.CreateGuid);
 
                    if (@event != null)
                    {
                        @event.ActionDate = question.CreationDate.HasValue ? question.CreationDate.Value : DateTime.Now;
                        @event.ActionAuthor = question.CreationAuthor;
                        @event.UnitOfWorkUid = Guid.Empty;
 
                        @event.ExtendedProperties[Common.Managers.EntityActionHistoryManager.ExtendedProperties_Uid] = Guid.Empty;
                        @event.ExtendedProperties[Common.Managers.EntityActionHistoryManager.ExtendedProperties_SessionUid] = null;
 
                        result.Add(@event);
                    }
                }
            }
        }
 
        return result;
    }
 
    #endregion
}

Later on, these events are processed and history items are generated according to them.

Creating a provider for displaying history item

Now we must specify the history items that we will display. To do that, we need to implement the EleWise.ELMA.Web.Mvc.ExtensionPoints.IHistoryPartProvider extension point or inherit one of the base types of history items: 

  • EleWise.ELMA.BPM.Web.Common.Components.CommentHistoryPartProviderBase – base provider for displaying items with comments
  • EleWise.ELMA.BPM.Web.Common.Components.AttachmentHistoryPartProviderBase - base provider for displaying items with attachments
  • EleWise.ELMA.BPM.Web.Tasks.Components.QuestionHistoryPartProviderBase - base provider for displaying items with questions / answers

We will inherit the CommentHistoryPartProviderBase and AttachmentHistoryPartProviderBase classes:

/// <summary>
/// Provider for the object’s history item 
/// Adds an item with a comment to the ‘Calendar event’ entity
/// </summary>
[Component]
public class CalendarEventCommentHistoryPartProvider : CommentHistoryPartProviderBase
{
    #region Overrides of CommentHistoryPartProviderBase
 
    /// <summary>
    /// Check if the entity is ‘Calendar event’
    /// After checking, the ‘Comments’ buttons will be added to the history /// panel
    /// </summary>
    /// <param name="html"></param>
    /// <param name="entity">Entity</param>
    /// <returns><c>true</c>, if the entity is ‘Calendar event' and we need /// to add the button to the history panel</returns>
    protected override bool CheckEntity(HtmlHelper html, IEntity entity)
    {
        return entity is ICalendarEvent;
    }
 
    /// <summary>
    /// Check if the created history data model is for ‘Calendar event’           /// object
    /// If the model has correct type, the history item will be sent for displaying
    /// </summary>
    /// <param name="html"></param>
    /// <param name="eventData"> Data for displaying</param>
    /// <returns><c>true</c>, if a model has correct type and can be displayed</returns>
    protected override bool CheckEventActionObject(HtmlHelper html, ICommentedHistoryModel eventData)
    {
        return eventData is ICalendarEventHistoryModel;
    }
 
    #endregion
}

Here you can see how we use the empty interface ICalendarEventHistoryModel mentioned above. In the same way we implement the heir of the attachment’s provider base class AttachmentHistoryPartProviderBase.

Note that this provider is an extension point and its implementations are called every time ELMA renders the history panels for all data models. Thus, you must clearly define the data you want to display for your objects. Otherwise, they will appear in the history panels of other objects.

Processing data and creating a model for a view

Next, we need to prepare data for display in history. To do that, you need to create a new class and inherit it from the EleWise.ELMA.Web.Mvc.Models.History.BaseAuditEventRender base class (it is the front end component).

/// <summary>
/// History handler for ‘Calendar event’ entity
/// </summary>
[Component(Order = 100)]
public class CalendarEventSimpleEventRender : BaseAuditEventRender
{
    /// <summary>
    /// List of GUIDs of actions that can be displayed
    /// </summary>
    protected override IEnumerable<Guid> Actions
    {
        get
        {
            yield return CalendarEventActions.AddCommentGuid;
            yield return CalendarEventActions.EditGuid;
        }
    }
 
    /// <summary>
    /// List of GUIDs of object types that can be displayed
    /// </summary>
    protected override IEnumerable<Guid> Objects
    {
        get { yield return EleWise.ELMA.Model.Services.InterfaceActivator.UID<ICalendarEvent>(); }
    }
 
    #region Implementation of IAuditEventRender
         
    /// <summary>
    /// Get data model of history item
    /// </summary>
    /// <param name="html">Helper</param>
    /// <param name="event">Event</param>
    /// <param name="historyLoader">History loader</param>
    /// <returns></returns>
    protected override IHistoryBaseModel CreateEventData(HtmlHelper html, EntityActionEventArgs @event, IEntityActionHistoryLoader historyLoader)
    {
        if (html == null) throw new ArgumentNullException("html");
        if (@event == null) throw new ArgumentNullException("event");
        if (historyLoader == null) throw new ArgumentNullException("historyLoader");
 
        if (@event.Action.Uid == CalendarEventActions.EditGuid)
            return RenderUserEdit(html, @event, historyLoader);
 
        if (@event.Action.Uid == CalendarEventActions.AddCommentGuid)
            return RenderAddComment(html, @event, historyLoader);
 
        return null;
    }
 
    private IHistoryBaseModel RenderUserEdit(HtmlHelper html, EntityActionEventArgs @event, IEntityActionHistoryLoader historyLoader)
    {
        if (historyLoader == null) throw new ArgumentNullException("historyLoader");
 
        var userEditEvent = new EditCalendarEventHistoryModel(@event, EleWise.ELMA.SR.T("Event changed"));
 
        var calendarEvent = (ICalendarEvent)@event.New;
 
        var editEvent = historyLoader.LoadHistory(
            @event.UnitOfWorkUid,
            EleWise.ELMA.Model.Services.InterfaceActivator.UID<ICalendarEvent>(),
            EleWise.ELMA.Model.Actions.DefaultEntityActions.UpdateGuid,
            calendarEvent.Id).FirstOrDefault();
 
        if (editEvent != null)
        {
            userEditEvent.OldEntity = (ICalendarEvent)editEvent.Old;
            userEditEvent.NewEntity = (ICalendarEvent)editEvent.New;
 
            var editEventArgs = editEvent as EditEntityActionEventArgs;
            if (editEventArgs != null)
            {
                userEditEvent.ChangedProperties = editEventArgs.ChangedProperties.ToList();
            }
        }
 
        var commentEvents = historyLoader.LoadHistory(
            @event.UnitOfWorkUid,
            EleWise.ELMA.Model.Services.InterfaceActivator.UID<EleWise.ELMA.Common.Models.IComment>(),
            EleWise.ELMA.Model.Actions.DefaultEntityActions.CreateGuid);
 
        var comment = commentEvents
            .Select(e => e.New)
            .Cast<EleWise.ELMA.Common.Models.IComment>()
            .FirstOrDefault();
 
        userEditEvent.Comment = comment;
 
        var attachEvents = historyLoader.LoadHistory(
            @event.UnitOfWorkUid,
            EleWise.ELMA.Model.Services.InterfaceActivator.UID<EleWise.ELMA.Common.Models.IAttachment>(),
            EleWise.ELMA.Model.Actions.DefaultEntityActions.CreateGuid);
 
        var attaches = attachEvents
            .Select(e => e.New)
            .Cast<EleWise.ELMA.Common.Models.IAttachment>()
            .ToList();
 
        userEditEvent.Attachments = attaches;
 
        return userEditEvent;
    }
 
    private IHistoryBaseModel RenderAddComment(HtmlHelper html, EntityActionEventArgs @event, IEntityActionHistoryLoader historyLoader)
    {
        if (historyLoader == null) throw new ArgumentNullException("historyLoader");
 
        var commentEvent = new CommentCalendarEventHistoryModel(@event, EleWise.ELMA.SR.T("Comment added"));
 
        var commentEvents = historyLoader.LoadHistory(
            @event.UnitOfWorkUid,
            EleWise.ELMA.Model.Services.InterfaceActivator.UID<EleWise.ELMA.Common.Models.IComment>(),
            EleWise.ELMA.Model.Actions.DefaultEntityActions.CreateGuid);
 
        var comment = commentEvents
            .Select(e => e.New)
            .Cast<EleWise.ELMA.Common.Models.IComment>()
            .FirstOrDefault();
 
        commentEvent.Comment = comment;
 
        var attachEvents = historyLoader.LoadHistory(
            @event.UnitOfWorkUid,
            EleWise.ELMA.Model.Services.InterfaceActivator.UID<EleWise.ELMA.Common.Models.IAttachment>(),
            EleWise.ELMA.Model.Actions.DefaultEntityActions.CreateGuid);
 
        var attaches = attachEvents
            .Select(e => e.New)
            .Cast<EleWise.ELMA.Common.Models.IAttachment>()
            .ToList();
 
        commentEvent.Attachments = attaches;
 
        return commentEvent;
    }
 
    /// <summary>
    /// Get additional item to display in history
    /// E.g. item with info about changes
    /// </summary>
    /// <returns>Item to add to the history, else <c>null</c></returns>
    protected override HistoryPartViewBlock GetExtraViewBlock(EntityActionEventArgs @event)
    {
        if (@event.Action.Uid == CalendarEventActions.EditGuid)
            return EditViewBlock();
 
        return null;
    }
 
    private HistoryPartViewBlock EditViewBlock()
    {
        return new HistoryPartViewBlock
        {
            HistoryPartType = "action",
            Index = 1,
            RenderDelegate = (html, model) => html.Partial("AuditView/CalendarEvent.Edit", model)
        };
    }
 
    #endregion
}

The main purpose of this class is to create correct data for displaying a history item according to the event data (EntityActionEventArgs) from the database.

You can also create a class and inherit it right from the EleWise.ELMA.Web.Mvc.ExtensionPoints.IAuditEventRender to implement completely custom logic for displaying history items, but we do not recommend doing that. 

Adding custom history items

In some cases, you might want to add custom history items or custom buttons to the history panel. To do that, you can implement the EleWise.ELMA.Web.Mvc.ExtensionPoints.IHistoryPartProvider extension point and create your own base class to add history items and/or buttons.

Below is the example of a base class to display a history item with a comment:

/// <summary>
/// Base provider for object’s history item 
/// Adds a button to the history panel and a history item with a comment
/// </summary>
public abstract class CommentHistoryPartProviderBase : IHistoryPartProvider
{
    public const string HistoryPartType = "comment";
 
    /// <summary>
    /// Get a set of history panel buttons
    /// </summary>
    /// <param name="html"></param>
    /// <param name="entity">Entity for displaying the history</param>
    /// <returns></returns>
    public virtual IEnumerable<HistoryPartButton> GetButtons(HtmlHelper html, IEntity entity)
    {
        if (CheckEntity(html, entity))
        {
            yield return new HistoryPartButton
                                {
                                    HistoryPartType = HistoryPartType,
                                    ImageUrl = html.Url().Image("#x16/add_comment.gif"),
                                    Index = 0,
                                    Text = SR.T("Comments")
                                };
        }
    }
 
    /// <summary>
    /// Check the entity
    /// After checking, the ‘Comments’ button will be added to the history /// panel
    /// </summary>
    /// <param name="html"></param>
    /// <param name="entity">Entity</param>
    /// <returns><c>true</c>, if entity has correct type and we need to add a button to the history panel </returns>
    protected abstract bool CheckEntity(HtmlHelper html, IEntity entity);
 
    protected virtual MvcHtmlString RenderDelegate(HtmlHelper html, IHistoryBaseModel eventData)
    {
        if (eventData is ICommentedHistoryModel)
        {
            var comment = ((ICommentedHistoryModel)eventData).Comment;
            if(comment != null)
            {
                return html.Partial("HistoryParts/Comment", eventData);
            }
        }
 
        return null;
    }
 
    /// <summary>
    /// Get a set of history items for the history model
    /// </summary>
    /// <param name="html"></param>
    /// <param name="eventData">Data of one history item</param>
    /// <returns></returns>
    public virtual IEnumerable<HistoryPartViewBlock> GetBlocks(HtmlHelper html, IHistoryBaseModel eventData)
    {
        if (CheckEventData(html, eventData))
        {
            yield return new HistoryPartViewBlock
                                {
                                    HistoryPartType = HistoryPartType,
                                    Index = 0,
                                    RenderDelegate = RenderDelegate
                                };
        }
    }
 
    /// <summary>
    /// Check if the model data has correct type
    /// </summary>
    /// <param name="html"></param>
    /// <param name="eventData">Data of model</param>
    /// <returns><c>true</c>, if needs to display the data</returns>
    protected virtual bool CheckEventData(HtmlHelper html, IHistoryBaseModel eventData)
    {
        var attachedData = eventData as ICommentedHistoryModel;
        return attachedData != null && CheckEventActionObject(html, attachedData);
    }
 
    /// <summary>
    /// Check if the generated history’s data model has correct type
    /// If the history’s data model has correct type, the item will be displayed
    /// </summary>
    /// <param name="html"></param>
    /// <param name="eventData">Data for displaying</param>
    /// <returns><c>true</c>, if the history’s data model has correct type and can be displayed</returns>
    protected abstract bool CheckEventActionObject(HtmlHelper html, ICommentedHistoryModel eventData);
}