How to manage external objects with ISupportsDynamicFields

From Sense/Net Wiki
Jump to: navigation, search
  •  
  •  
  •  
  •  
  • 100%
  • 6.0
  • Enterprise
  • Community
  • Planned

Overview

If you want you can manage external content as if they were stored in the Sense/Net Content Repository. A typical example is when you store data in a line of business application and you would like to map them to Sense/Net for browsing and editing. For more details please check the External objects article. This page describes how can you manage external object in Sense/Net using ISupportsDynamicFields interface. This method supports displaying or editing individual items, displaying lists of external items or handle them in more complex scenarios. For an easier and simpler solution you should use RuntimeContentHandler.

Details

You can wrap legacy types into regular portal content and display or edit them with well-known mechanisms: collection or editor portlets, using content views and field controls. To achieve this you should follow the steps below.

Editing and creating external objects

In this section you can learn how can you define the necessary portal elements for editing or creating external objects.

The external object

We wil use the following external object as an example. It has two simple fields that both will be displayed and edited on the portal.

public class MyExternalObject
{
    public string ExtName { get; set; }
    public int Value { get; set; }
}

Create a content handler

You have to create a special Content Handler for wrapping the external object. This class will provide an API (a custom constructor or init method that lets you pass the external object to wrap) and will be used as a content handler when creating wrapper Content for the external object.

using System.Collections.Generic;
using SenseNet.ContentRepository;
using SenseNet.ContentRepository.Fields;
using SenseNet.ContentRepository.Schema;
using SenseNet.ContentRepository.Storage;
 
namespace SenseNet.Portal
{
    [ContentHandler]
    public class ExternalItem :  GenericContent, ISupportsDynamicFields
    {
        private MyExternalObject _extObj;
 
        public ExternalItem(Node parent) : this(parent, null)
        {
            InitExtObject();
        }
        public ExternalItem(Node parent, string nodeTypeName) : base(parent, nodeTypeName)
        {
            InitExtObject();
        }
        protected ExternalItem(NodeToken nt) : base(nt)
        {
            InitExtObject();
        }
 
        public ExternalItem(MyExternalObject extObject, Node parent) : base(parent, null)
        {
            _extObj = extObject;
            //Initialize is skipped because it overrides 
            //the current property values with the default values.
            //base.Initialize();
        }
 
        protected override void Initialize()
        {
            //do nothing
        }
 
        private void InitExtObject()
        {
            _extObj = new MyExternalObject();
        }
    }
}

The InitExtObject method is needed when the caller method does not provide an existing external object to edit, e.g. in a create scenario - that is also possible with the portal infrastructure.

You have to redirect the property get and set mechanism to provide and modify the properties of the wrapped type instead of trying to write values to the repository. Add the following method overrides to the class above:

public override object GetProperty(string name)
{
    switch (name)
    {
        case "Name": 
        case "ExtName":
            return _extObj.ExtName;
        case "Value":
            return _extObj.Value;        
    }
 
    return base.GetProperty(name);
}
 
public override void SetProperty(string name, object value)
{
    switch (name)
    {
        case "Name":
        case "ExtName":
            this._extObj.ExtName = value as string;
            return;
        case "Value":
            this._extObj.Value = (int)value;
            return;
    }
 
    base.SetProperty(name, value);
}

Your class implements the ISupportsDynamicFields interface. The following code demonstrates how to implement the necessary methods. You need to provide all the field metadata for the fields that will be displayed and edited. This mechanism replaces the field definitions that you would normally write into a content type definition XML.

#region ISupportsDynamicFields Members
 
public IDictionary<string, FieldMetadata> GetDynamicFieldMetadata()
{
    //TODO:create field metadata for every property of the external 
    //object that you want to edit or display on the UI
 
    //The default value for a field is evaluated only when a new content is created.
 
    return new Dictionary<string, FieldMetadata>
               {
                   {
                       "ExtName", new FieldMetadata
                                      {
                                          FieldName = "ExtName",
                                          CanRead = true,
                                          CanWrite = true,
                                          FieldSetting = new ShortTextFieldSetting
                                                             {
                                                                 Name = "ExtName",
                                                                 DisplayName = "External name",
                                                                 Description = "Description for the field",
                                                                 FieldClassName = typeof (ShortTextField).FullName
                                                             }
                                      }
                       },
                   {
                       "Value", new FieldMetadata
                                    {
                                        FieldName = "Value",
                                        CanRead = true,
                                        CanWrite = true,
                                        FieldSetting = new IntegerFieldSetting
                                                           {
                                                               Name = "Value",
                                                               ShortName = "Integer",
                                                               DisplayName = "External value",
                                                               Description = "Description for the field",
                                                               FieldClassName = typeof (IntegerField).FullName,
                                                               DefaultValue = "3"
                                                           }
                                    }
                       }
               };
}
 
public bool IsNewContent
{
    get
    {
        //TODO:implement custom logic to determine if the external object is new or not
        return _extObj == null || string.IsNullOrEmpty(_extObj.ExtName);
    }
}
 
#endregion


The predefined field settings that you can use to define your fields are the following (it is possible to write a custom field setting if you need one):

  • BinaryFieldSetting
  • ChoiceFieldSetting
  • CurrencyFieldSetting
  • DateTimeFieldSetting
  • HyperLinkFieldSetting
  • IntegerFieldSetting
  • LongTextFieldSetting
  • NullFieldSetting
  • NumberFieldSetting
  • PasswordFieldSetting
  • PermissionChoiceFieldSetting
  • ReferenceFieldSetting
  • ShortTextFieldSetting
  • TextFieldSetting
  • XmlFieldSetting
  • YesNoFieldSetting


To avoid saving or deleting nonexisting content in the Content Repository, you should override the following methods. This is the place where you should implement saving and deleting items from the external database.

public override void SaveSameVersion()
{
    //TODO:save external object to the external db
}
 
public override void Save(SavingMode mode)
{
    //TODO:save external object to the external db
}
 
public override void Delete()
{
    //TODO:delete external object
}

Define a content type

You should define a Content Type for the external type first and install it to the Content Repository. This will be an empty content type definition (without fields), because the fields are defined in your source code, as seen above. The CTD may contain field definitions that override some inherited fields (e.g. DisplayName or Index, defined on GenericContent) to change their visibility.

Name the new content type as ExternalItem and set the full class name defined above as the content handler in the CTD (in this case SenseNet.Portal.ExternalItem).

<ContentType name="ExternalItem" parentType="GenericContent" handler="SenseNet.Portal.ExternalItem" xmlns="http://schemas.sensenet.com/SenseNet/ContentRepository/ContentTypeDefinition">
	<DisplayName>My External item</DisplayName>
	<Description>ExternalItem handles external objects</Description>
	<Icon>File</Icon>
    <Fields>
    </Fields>
</ContentType>

Creating a folder for external objects

To display a list of external objects (e.g. existing ones provided by a web service or a custom database) you need to write some source code to collect the items and wrap them into the external item content handler we defined in the previous section. The easiest way to do this is to implement a custom folder content type that provides the external items as a reference field.

The following example shows how to implement a custom content handler for the new content type ExternalFolder. The most important thing here is the ExternalObjects field that will provide the list of existing items. This field can be used in various scenarios on the portal, e.g. when using a content collection portlet or a Sense/Net data source control.

[ContentHandler]
public class ExternalFolder : Folder
{
    public ExternalFolder(Node parent) : this(parent, null) { }
	public ExternalFolder(Node parent, string nodeTypeName) : base(parent, nodeTypeName) { }
    protected ExternalFolder(NodeToken nt) : base(nt) { }
 
    public const string EXTERNALOBJECTS = "ExternalObjects";
    [RepositoryProperty(EXTERNALOBJECTS, RepositoryDataType.Reference)]
    public IEnumerable<ExternalItem> ExternalObjects
    {
        get
        {
            var items = GetExternalObjects();
            return items.Select(i => new ExternalItem(i, this));
        }
    }
 
    public IEnumerable<MyExternalObject> GetExternalObjects()
    {
        //TODO:retrieve items from an external source
        return new List<MyExternalObject>
                        {
                            new MyExternalObject {ExtName = "MyItem1", Value = 456 }, 
                            new MyExternalObject {ExtName = "MyItem2", Value = 789 }
                        };
    }
 
    public override object GetProperty(string name)
    {
        switch (name)
        {
            case EXTERNALOBJECTS:
                return this.ExternalObjects;
            default:
                return base.GetProperty(name);
        }
    }
 
    public override void SetProperty(string name, object value)
    {
        switch (name)
        {
            case EXTERNALOBJECTS:
                //do nothing, this is a readonly technical property
                break;
            default:
                base.SetProperty(name, value);
                break;
        }
    }
}

You need to install a content type XML for this type to let the portal recognise the new type.

<?xml version="1.0" encoding="utf-8"?>
<ContentType name="ExternalFolder" parentType="Folder" handler="SenseNet.Portal.ExternalFolder" xmlns="http://schemas.sensenet.com/SenseNet/ContentRepository/ContentTypeDefinition">
  <DisplayName>External Folder</DisplayName>
	<Description>Use External Folder to display external items as children content.</Description>
	<Icon>SmartFolder</Icon>
  <Fields>
    <Field name="ExternalObjects" type="Reference">
      <DisplayName>External objects</DisplayName>
      <Configuration>
        <AllowMultiple>true</AllowMultiple>
      </Configuration>
    </Field>
    <Field name="AllowedChildTypes" type="AllowedChildTypes">
      <Configuration>
        <VisibleBrowse>Hide</VisibleBrowse>
        <VisibleEdit>Show</VisibleEdit>
        <VisibleNew>Show</VisibleNew>
      </Configuration>
    </Field>
  </Fields>
</ContentType>

Create an Edit content view

To display an individual external object you need to create a regular Content View for it. You can use common field controls to edit the fields you defined in the content handler.

  • Create a new Empty Content view folder (this is a container of type ContentViews) under /Root/Global/contentviews. Name it ExternalItem. This will hold the edit Content View for our external items.
  • Create a new edit content view in the previously created /Root/Global/contentviews/ExternalItem folder and name it Edit.ascx. Paste the following source into the content view:
<%@ Control Language="C#" AutoEventWireup="true" Inherits="SenseNet.Portal.UI.SingleContentView" %>
 
<sn:ShortText runat="server" ID="ShortTextField" FieldName="ExtName" />
<sn:WholeNumber runat="server" ID="NumberField" FieldName="Value" />
 
<div class="sn-panel sn-buttons">
  <sn:CommandButtons ID="CommandButtons1" runat="server"/>
</div>
Edit view

Create an editor portlet

You need a simple editor portlet to display a content that was made from an external object. This editor portlet inherits from ContentEditorPortlet and consists of a few lines of code that creates a content and loads a content view created in the previous step. After editing the object it can be saved to the external database.

Put the following lines into the source code of the portlet:

private ContentView _contentView;
private MyExternalObject _myExtObj;
 
public MyExternalEditorPortlet()
{
    Name = "My External Editor Portlet";
    Description = "Editor portlet for external items";
    Category = new PortletCategory(PortletCategoryType.Content);
}
 
protected override void CreateChildControls()
{
    //TODO:load the item you want to edit from the external database or web service
    var ef = this.ContextNode as ExternalFolder;
    if (ef != null)
    {
        _myExtObj = ef.GetExternalObjects().FirstOrDefault(ei => ei.ExtName == HttpContext.Current.Request["itemID"]);
 
        var handler = new ExternalItem(_myExtObj, this.ContextNode);
        var content = Content.Create(handler);
 
        _contentView = ContentView.Create(content, Page, ViewMode.Edit, ContentViewPath);
        _contentView.CommandButtonsAction += contentView_CommandButtonsAction;
 
        this.Controls.Add(_contentView);
    }
    else
    {
        this.Controls.Add(new LiteralControl("The current content is not an external folder."));
    }
 
    ChildControlsCreated = true;
}

You need to implement the external object loading mechanism for your item. For example using a URL parameter or session variable to pass the identifier of the item to load.

When the user clicks the Save button, you only have to update the content and call the Save method and your external object will contain the data that the user gave on the UI.

protected override void OnCommandButtons(CommandButtonsEventArgs e)
{
    switch (e.ButtonType)
    {
        case CommandButtonType.CheckoutSave:
        case CommandButtonType.CheckoutSaveCheckin:
        case CommandButtonType.Save:
        case CommandButtonType.SaveCheckin:
            _contentView.UpdateContent();
            var content = _contentView.Content;
 
            if (!_contentView.IsUserInputValid || !content.IsValid)
                return;
 
            //Save input data to _myExtObj. After this line, _myExtObject
			//properties will contain all the data entered on the UI
            content.Save();
 
            //TODO: save _myExtObj to an external database
 
            CallDone();
            break;
    }
}

Create a page for the editor

The place of the editor page depends on the scenario you want to use it in. If the external object can be connected to one of the content in the Content Repository, you may create an Application for that content. For example an AddComment action for a blog post or an EditSubscription for a library. In our case we will create an application called EditExternalItem.

  • Create a folder called ExternalFolder under /Root/(apps). This is the name of the custom folder type we created in the previous section. This means the application will be defined for our custom folder type.
  • Create a Portlet Page in this folder called EditExternalItem. Select sn-layout-dialog as the page template. This page will hold the portlet for editing individual items.
  • You have to place the editor portlet created above onto this page (for more info see How to add a portlet to a page). You'll find your portlet in the Content category, as defined in the constructor above. As the view of the portlet, the previously created Edit.ascx will be selected automatically, you may leave the content view property empty. If you named it differently, please select the content view manually.
Adding external editor portlet

To be able to access this page, you need to place links somewhere in the portal (where these external objects are listed) that lead to this page. See the next section for more details.

Creating external objects

To learn how to add new items, please check the next section about displaying the Add link for the external type.

Displaying a list of external objects

Creating a collection view for the items

The next step is to create a collection view (an ASCX file) that displays a list of external items. The view will receive a collection of regular content provided by the portal, you will be able to use the same controls and methods as in any other collection views. The links pointing to the previously created edit action should contain a URL parameter for the particular object you want to display or edit. In this case we add a parameter called itemID that contains the name of the external object. This will be used by the editor portlet to find the item to edit.

  • Go to /Root/Global/renderers in Content Explorer
  • Add a new User control
  • Name it ExternalCollectionView.ascx and paste the following lines to the view:
<%@ Control Language="C#" AutoEventWireup="true" Inherits="SenseNet.Portal.Portlets.ContentCollectionView" %>
<%@ Import Namespace="System.Linq" %>
<%@ Import Namespace="SenseNet.Portal.Helpers" %>
 
<sn:ContextInfo runat="server" ID="CurrentInfo" />
 
<div class="sn-contentlist">
 
  <% foreach (var content in this.Model.Items)
  {   %>
    <div class="sn-content sn-contentlist-item">
      <h1 class="sn-content-title"><a href="?action=EditExternalItem&itemID=<%= content["Name"] %>&backtarget=parent">Edit <%= content["ExtName"] %></a></h1>        
    </div>
<%} %>
 
 
<sn:ActionLinkButton ID="AddExternalItem" runat="server" ActionName="Add" ParameterString="ContentTypeName=ExternalItem" Text="Add new External item" ContextInfoId="CurrentInfo" />
 
</div>​

As you can see in the example above, to provide a possibility to create a new external item is very simple: the built-in Add action is capable of displaying a generic view for the new item. Your previously defined content handler will handle the new scenario by adding new items to the external database. If you want to create a custom view for adding a new item, you can do so by creating a InlineNew.ascx, similar to the Edit.ascx defined previously for the edit page.

Displaying the list

The last step is to place a regular content collection portlet to the Explore page of the ExternalFolder type created above. To do this, do the following:

  • Copy the existing /Root/(apps)/GenericContent/Explore page to the folder /Root/(apps)/ExternalFolder.
  • Edit this page: place a Content collection Portlet to one of the portlet zones
  • Set the following properties of the portlet:
    • Renderer: choose the collection view created above (/Root/Global/renderers/ExternalCollectionView.ascx).
    • Collection source: reference property
    • Reference property name: ExternalObjects (the name of the reference field defined on the custom folder type)

Finally, you have to create a new content somewhere in the Content Repository (e.g. under Default site) of type ExternalFolder. To achieve this, you need to do the following:

  • Add ExternalFolder type as an allowed child type for Default site.
  • Add an external folder there: you will find your custom type in the New menu.
  • Add ExternalItem type as an allowed child type for this external folder.

When you select the external folder in Content Explorer, you should see the list of the external items on the explore page of the folder. The links should navigate to the edit or add page for the external objects.

My external folder

Related links

References

There are no external references for this article.