Implementing the possibility to pin an object to a page

This article provides an example of how to implement the possibility to pin an object to a page. There are many ways to use this capability. This article describes two use cases:

  1. Opening a specific instance of the IPaperPinExampleObject object when going to a module. IPaperPinExampleObject – is a simple object with standard fields: Name, Author, Date Created, Date Modified, Modified By.
  2. Displaying pinned objects (documents, in this example) to a portlet.

Example

 

Fig. 1. Pin icon on the object form

Fig. 2. Portlet displaying pinned document

Extension (interface) methods

IPaperPinProvider extension point (interface) has the following methods:

/// <summary>
/// Availability of the pin for this object
/// </summary>
/// <param name="typeUid">Object type GUID</param>
/// <param name="entityId">Object ID</param>
/// <returns><c>true</c>, if pin will be displayed for this object type</returns>
bool IsAvailable(Guid typeUid, object entityId = null);
         
/// <summary>
/// All supported types.
/// </summary>
/// <returns>List of supported types</returns>
List<Guid> AvailableTypes();
 
/// <summary>
/// Use to pin only one instance of this type. All the other instances are unpinned.
/// </summary>
/// <returns><c>true</c>, if it is used to pin only one instance of this type</returns>
bool OnlyOneInstance();
 
/// <summary>
/// Description on the button, when the object is not pinned.
/// </summary>
string TooltipText(Guid typeUid, object entityId = null);
 
/// <summary>
/// Description on the button, when the object is pinned.
/// </summary>
string TooltipTextPin(Guid typeUid, object entityId = null);
 
/// <summary>
/// Warning text displayed when pinning the object
 </summary>
/// <param name="typeUid">Object type GUID</param>
/// <param name="entityId">Object ID</param>
string ConfirmText(Guid typeUid, object entityId = null);

This extension point is implemented using the BasePaperPinProvider base class

BasePaperPinProvider has the following methods:

/// <summary>
/// Availability of the pin for this object
/// </summary>
/// <param name="typeUid"></param>
/// <param name="entityId">Object ID</param>
/// <returns><c>true</c>, if pin will be displayed for the specified object type</returns>
public virtual bool IsAvailable(Guid typeUid, object entityId = null)
{
    var availableTypes = AvailableTypes();
    return availableTypes.Any(uid => uid == typeUid);
}
 
/// <summary>
/// All the supported types.
/// </summary>
/// <returns>List of supported types</returns>
public abstract List<Guid> AvailableTypes();
 
/// <summary>
/// Use to pin only one instance of this type. All the other instances are unpinned
/// </summary>
/// <returns><c>true</c>, if it is used to pin only one instance of this type</returns>
public virtual bool OnlyOneInstance()
{
    return false;
}
 
/// <summary>
/// Description on the button, when the object is unpinned.
/// </summary>
/// <param name="typeUid">Object type GUID</param>
/// <param name="entityId">Object ID</param>
public abstract string TooltipText(Guid typeUid, object entityId = null);
 
/// <summary>
/// Description on the button, when the object is pinned.
/// </summary>
/// <param name="typeUid">Object type GUID</param>
/// <param name="entityId">Object ID</param>
public abstract string TooltipTextPin(Guid typeUid, object entityId = null);
 
/// <summary>
/// Warning text when pinning the object
/// </summary>
/// <param name="typeUid">Object type GUID</param>
/// <param name="entityId">Object ID</param>
public abstract string ConfirmText(Guid typeUid, object entityId = null);

Example 1. Opening a specific instance of the IPaperPinExampleObject object

Extension point class example

[Component]
public class ObjectPaperPinExample : BasePaperPinProvider
{
    public IEntityActionHandler EntityActionHandler { get; set; }
 
    /// <summary>
    /// Condition, under which the "pin" is available
    /// </summary>
    /// <param name="typeUid"></param>
    /// <param name="entityId"></param>
    /// <returns></returns>
    public override bool IsAvailable(Guid typeUid, object entityId = null)
    {
        if (!base.IsAvailable(typeUid, entityId))
            return false;
 
        if (entityId == null)
            return false;
 
        return typeUid == InterfaceActivator.UID<IPaperPinExampleObject>();
    }
 
    /// <summary>
    /// All the supported types.
    /// </summary>
    /// <returns></returns>
    public override List<Guid> AvailableTypes()
    {
        return AllObjectTypes();
    }
 
    public static List<Guid> AllObjectTypes()
    {
        return new List<Guid> { InterfaceActivator.UID<IPaperPinExampleObject>() };
    }
 
    /// <summary>
    /// Use to pin only one instance for this type. All the other instances are unpinned
    /// </summary>
    /// <returns></returns>
    public override bool OnlyOneInstance()
    {
        return true;
    }
 
    /// <summary>
    /// Description on the button, when the object is unpinned.
    /// </summary>
    public override string TooltipText(Guid typeUid, object entityId = null)
    {
        return SR.T("After clicking on the button, this object will open after selecting the \"My Objects\" menu item");
    }
 
    /// <summary>
    /// Description on the button, when the object is pinned.
    /// </summary>
    public override string TooltipTextPin(Guid typeUid, object entityId = null)
    {
        return SR.T("After clicking on the button, when selecting the \"My Objects\" menu item, the main page of the \"My Objects\" module will open");
    }
 
    /// <summary>
    /// Warning text when pinning the object
    /// </summary>
    /// <param name="typeUid"></param>
    /// <param name="entityId"></param>
    public override string ConfirmText(Guid typeUid, object entityId = null)
    {
        return SR.T("The object will open after selecting the \"My Objects\" menu item");
    }
}
Note
After implementing the extension point, the object will have a pin. You need to implement a logic for this pi

In this example, a menu item is created, which opens a list of objects for the user:

protected IUser CurrentUser
{
    get
    {
        return AuthenticationService.GetCurrentUser<IUser>();
    }
}
 
[ContentItem]
public ActionResult Grid()
{
    // if an object is pinned, open it 
    var paperPinManager = PaperPinManager.Instance;
    var allObjectTypes = ObjectPaperPinExample.AllObjectTypes();
    var pin = paperPinManager.FirstPaperPinByUser(CurrentUser, allObjectTypes);
    if (pin != null && !string.IsNullOrEmpty(pin.EntityId))
    {
        long entityId;
        if (long.TryParse(pin.EntityId, out entityId))
            return RedirectToAction("ViewItem", new { id = entityId });
    }
 
    var filter = CreateFilter();
    var data = CreateGridData(new GridCommand(), filter);
    return View(data);
}
 
public ActionResult ViewItem(long id)
{
    var model = PaperPinExampleObjectManager.Instance.Load(id);
    return View(model);
}

As you can see, when selecting the menu item, if the first pinned object is found, its instance will open.

To learn more about creating a menu item, read this article.

Note
When implementing an object page, add the following code to the header, where Model is an IPaperPinExampleObject object
@{
    var title = SR.T("Object - {0}", Model.Name);
    Html.Header(title, Model);
}

 This code will add a pin to the page.

Example 2. Writing pinned objects (documents in the example) to a portlet

Extension point class example

    private static List<Guid> _documentAvailableTypes;
 
    public static List<Guid> DocumentAvailableTypes
    {
        get
        {
            if (_documentAvailableTypes == null)
            {
                _documentAvailableTypes = Locator.GetServiceNotNull<IMetadataRuntimeService>()
                    .GetMetadataList()
                    .OfType<DocumentMetadata>()
                    .Select(documentMetadata => documentMetadata.Uid).ToList();
            }
            return _documentAvailableTypes;
        }
    } 
 
 
[Component]
public class DocumentPaperPinExample : BasePaperPinProvider
{
    public IEntityActionHandler EntityActionHandler { get; set; }
 
    /// <summary>
    /// Condition, under which the "pin" is available
    /// </summary>
    /// <param name="typeUid"></param>
    /// <param name="entityId"></param>
    /// <returns></returns>
    public override bool IsAvailable(Guid typeUid, object entityId = null)
    {
        if (!base.IsAvailable(typeUid, entityId))
            return false;
 
        if (entityId == null)
            return false;
 
        return DocumentAvailableTypes.FirstOrDefault(a => a == typeUid) != null;
    }
 
    /// <summary>
    /// All the supported types.
    /// </summary>
    /// <returns></returns>
    public override List<Guid> AvailableTypes()
    {
        return DocumentAvailableTypes;
    }
 
    /// <summary>
    /// Description on the button, when the object is unpinned.
    /// </summary>
    public override string TooltipText(Guid typeUid, object entityId = null)
    {
        return SR.T("After clicking on the button, the document will be added to the \"Pinned documents list\" portlet");
    }
 
    /// <summary>
    /// Description on the button, when the object is pinned.
    /// </summary>
    public override string TooltipTextPin(Guid typeUid, object entityId = null)
    {
        return SR.T("The document is displayed in the \"Pinned documents list\" portlet");
    }
 
    /// <summary>
    /// Warning text when pinning the object
    /// </summary>
    /// <param name="typeUid"></param>
    /// <param name="entityId"></param>
    public override string ConfirmText(Guid typeUid, object entityId = null)
    {
        return SR.T("The document will be added to the \"Pinned documents list\" portlet");
    }
}

Create a portlet in the module. To learn more about creating portlets in a module, read this article.

The portlet view has the following code:

@model PaperPinExampleModule.Web.Portlets.PaperPinDocumentsPersonalization
@using PaperPinExampleModule.Web
 
@Html.Action("PortletView", "Home", new {area = RouteProvider.AreaName})

The PortletView method of the HomeController controller is called in the view

Controller method code:

protected IUser CurrentUser
{
    get
    {
        return AuthenticationService.GetCurrentUser<IUser>();
    }
}
 
public ActionResult PortletView()
{
    var documentManager = DocumentManager.Instance;
    //Get all the IDs of the documents, pinned for the current user
    var paperPinManager = PaperPinManager.Instance;
    var allObjectTypes = DocumentPaperPinExample.DocumentAvailableTypes;
    var pins = paperPinManager.GetAllPaperPinsByUser(CurrentUser, allObjectTypes);
             
    List<long> documentIdsArray = new List<long>();
    foreach (var pin in pins)
    {
        long documentId;
        if (long.TryParse(pin.EntityId, out documentId))
            documentIdsArray.Add(documentId);
    }
    var docFilter = InterfaceActivator.Create<IDocumentFilter>();
    docFilter.Ids = documentIdsArray;
     
    var fetchOptions = new FetchOptions
    {
         SelectColumns = new List<string>{"Id", "Name"}
    };
     
    var documents = documentManager.Find(docFilter, fetchOptions).Select(doc => new DocumentPortletModel()
    {
        Name = doc.Name,
        Id = doc.Id
    }).ToList();
     
    return PartialView("PortletView", documents);
}

Where DocumentPortletModel - is a class, representing a simple model with Name and Id properties:

public class DocumentPortletModel
{
    /// <summary>
    /// Document name
    /// </summary>
    public string Name { get; set; }
 
    /// <summary>
    /// Document ID
    /// </summary>
    public long Id { get; set; }
}

As you can see, only the documents, pinned by the current users, are added to the document list, therefore the portlet will display pinned documents only for the current user. Next, the controller returns a partial view PortletView. The partial view code:

@model System.Collections.Generic.List<PaperPinExampleModule.Web.Models.DocumentPortletModel>
 
<style>
    .tableTd {
        padding: 3px 5px !important;
        border-bottom: 1px solid #e6e6e6 !important;
    }
</style>
 
<table class="t-grid-table" style="width: 100%">
    <thead class="t-grid-header">
        <tr>
            <th scope="col" class="t-header "><span class="t-header-content">@SR.T("Название документа")</span></th>
        </tr>
    </thead>
    @if (Model.Count > 0)
    {
        foreach (var doc in (dynamic)Model)
        {
            if (doc != null)
            {
                <tr>
                    <td class="tableTd">
                        <a href="@Url.Action("View", "Document", new {area = "EleWise.ELMA.Documents.Web", id = doc.Id})">@doc.Name</a>
                    </td>
                </tr>
            }
        }
    }
</table>
Important
In the second example, the OnlyOneInstance method is missing. This is due to the fact that the BasePaperPinProvider base class defines this method as false since the portlet must display all the pinned documents instead of only one.