Archive

Posts Tagged ‘Object Model’

SharePoint: Understanding BusinessData Column (BDC Field)

December 6th, 2011 No comments

    This is the logical continuation of the article SharePoint: Brief introduction to Business Data Catalog (BDC)

Business Data Catalog (BDC) provides SharePoint with an ability to display, choose and store data from such external data sources as Web services and DataBases. Data item from external data source (either a record from a database or an object returned by a Web service) might be called BDC Entity Instance. BDC Column (or BDC Field, they are synonyms) is a column of ‘Business Data’ type. Once BDC Column is added to a list, users will be able to choose a BDC Entity Instance from the BDC to store as the value of the BDC Column.

BDC Column can be considered as a complex field, because technically it’s a group of several logically related SP fields. Adding one ‘Business Data’ column to the list, at least two fields will be added to the list in fact. The first is intended to contain a display value that will be shown to the user, while the second is intended to store an encoded identifier of the chosen BDC Entity Instance.

Let’s take a look at an example. Let’s assume that we have a Business Data Catalog that provides us with the Product object with the following structure:

Product 
{
	ID,
	Name,
	Price,
	Producer
}

If we create a new ‘Business Data’ column on a list as it shown in the picture below,
Add Business Data Column
the list schema exposes the following new fields:

<Field Type="BusinessData" DisplayName="Product" 
Required="FALSE" ID="{bc203358-6113-470f-9b08-f6100cc034f2}" 
StaticName="Product" BaseRenderingType="Text" Name="Product" 
SystemInstance="ExternalProductDB_Instance" Entity="Products" 
BdcField="Name" Profile="" HasActions="False" 
SecondaryFieldBdcNames="Price:Producer" RelatedField="Products_ID" 
SecondaryFieldWssNames="Product_x003a__x0020_Price:Product_x003a__x0020_Producer" 
RelatedFieldBDCField="" RelatedFieldWssStaticName="Products_ID" 
SecondaryFieldsWssStaticNames="Product_x003a__x0020_Price:Product_x003a__x0020_Producer" />
  
<Field Type="Note" DisplayName="Products_ID" Hidden="TRUE" ReadOnly="TRUE"
BdcField="Products_ID" ID="{0d37c424-0e57-4429-8f92-0b8faec5a5bd}" 
StaticName="Products_ID" Name="Products_ID" />

<Field Type="Note" DisplayName="Product: Price" ReadOnly="TRUE" 
BdcField="Price" ID="{c56d7123-cb3f-46f4-8f45-874ac5cee13d}" 
StaticName="Product_x003a__x0020_Price" Name="Product_x003a__x0020_Price" />

<Field Type="Note" DisplayName="Product: Producer" ReadOnly="TRUE" 
BdcField="Producer" ID="{df740ff6-2998-46e1-ac40-974d66ba7dc7}" 
StaticName="Product_x003a__x0020_Producer" Name="Product_x003a__x0020_Producer" />

*Note: some redundant attributes of fields are skipped

We have four added SP Fields that are logically related. As I mentioned before, the first field (Name=”Product”) is intended to store a value, which is displayed to the user. Besides, the field contains a number of attributes that refer to all of the rest of logically related fields. We’ll study these attributes a bit later. The second field (Name=”Products_ID”) is a hidden field, which is intended to store an encoded identifier of the BDC Entity Instance. The encoded identifier usually looks like “__dk410035008400140025005400k410…”. It’s permissible if more than one BDC Entity fields compose the identifier.

Other two additional fields (Name=”Product_x003a__x0020_Price” and Name=”Product_x003a__x0020_Producer”) are to store and show such properties of the chosen BDC Entity Instance as Price and Producer. These additional fields are optional, you might be unwilling to have any extra information in the list.

Ok, let’s examine closer the first field (Name=”Product”). It includes the attributes that describe what properties of the chosen BDC Entity Instance will be captured and what additional SP fields they will be stored in. The most important attributes are shown below:

  • SystemInstance – the name of the Lob System Instance or Business Data Catalog Application Instance, which BDC Column is bound to;
  • Entity – the type name of returned business data object, which BDC Column is bound to;
  • BdcField – the name of BDC Entity field, the value of which will be stored in this SharePoint field. For the Product field specifically, the stored value will be displayed to the user;

  • RelatedField – the internal name of a hidden SharePoint field, which is to store an encoded identifier of the chosen BDC Entity Instance;
  • RelatedFieldBDCField – usually an empty string, probably, it’s reserved for future usage, or it’s a legacy attribute;
  • RelatedFieldWssStaticName – the static name of a hidden SharePoint field, which stores an encoded identifier of the chosen BDC Entity Instance;

  • SecondaryFieldBdcNames – the colon-separated names of the fields within BDC Entity, the values of those fields are to be stored in the additional SharePoint fields. The attribute contains the same number of names as the SecondaryFieldWssNames attribute;
  • SecondaryFieldWssNames – the colon-separated internal names of the additional SharePoint fields, that are to store additional values from the chosen BDC Entity Instance. The attribute contains the same number of names as the SecondaryFieldBdcNames attribute;
  • SecondaryFieldsWssStaticNames – the colon-separated static names of the additional SharePoint fields, that are to store additional values from the chosen BDC Entity Instance. The attribute contains the same number of names as the SecondaryFieldBdcNames and SecondaryFieldWssNames attributes;

All of the rest of fields have BdcField-attributes as well. The attribute contains the name of BDC Entity field, the value of which will be stored in corresponding SharePoint field. But I’ve found an exception. This exception is the hidden field, which is intended to store an encoded identifier. In the given case it’s the Products_ID field. The BDCField-attribute of such fields always contains an invalid value, which equals to the name of the field itself. In our case is BdcField=”Products_ID”, and there is no BDC Entity field with the name Products_ID defined either in DataBase or Application Definition File. And the same situation is with all BusinessDate SharePoint fields I investigated.

Below is a summary schema that exposes what BusinessData column attributes refer to

BDC Field Schema Explanation

To work with BusinessData field programmatically we can cast SPField-object to BusinessDataField type defined in the Microsoft.SharePoint.Portal.WebControls namespace:

using Microsoft.SharePoint.Portal.WebControls;
...
SPList spList = ...
SPField spField = spList.Fields["display name of some field"];
BusinessDataField bizDataField = (BusinessDataField)spField;

To get values of attributes and to get all logically related SharePoint fields we can use the following code:

...
string systemInstance = bizDataField.SystemInstanceName;
string entity         = bizDataField.EntityName;
string bdcField       = bizDataField.BdcFieldName;

// get info about related field, which is to store the encoded identifier of BDC Entity Instance
string relatedField              = bizDataField.RelatedField;
string relatedFieldBDCField      = bizDataField.GetProperty("RelatedFieldBDCField");
string relatedFieldWssStaticName = bizDataField.GetProperty("RelatedFieldWssStaticName");

// get info about additional fields, that are to store additional values from BDC Entity Instance
string[] secondaryFieldBdcNames = bizDataField.GetSecondaryFieldsNames();

string[] separators = new string[] { ":" };

string secondaryFieldWssNamesVal = bizDataField.GetProperty("SecondaryFieldWssNames");
string[] secondaryFieldWssNames = string.IsNullOrEmpty(secondaryFieldWssNamesVal) ? 
    new string[0] :
    secondaryFieldWssNamesVal.Split(separators, StringSplitOptions.RemoveEmptyEntries);

string secondaryFieldsWssStaticNamesVal = bizDataField.GetProperty("SecondaryFieldsWssStaticNames");
string[] secondaryFieldsWssStaticNames = string.IsNullOrEmpty(secondaryFieldsWssStaticNamesVal) ?
    new string[0] :
    secondaryFieldsWssStaticNamesVal.Split(separators, StringSplitOptions.RemoveEmptyEntries);
        
// get all logically related sp fields
SPField relatedSPField = spList.Fields.GetFieldByInternalName(relatedField);
List<SPField> secondarySPFields = new List<SPField>(secondaryFieldWssNames.Length);
foreach (string secondaryFieldName in secondaryFieldWssNames)
    secondarySPFields.Add(spList.Fields.GetFieldByInternalName(secondaryFieldName));

SharePoint: Brief introduction to Business Data Catalog (BDC)

November 4th, 2011 No comments

    Business Data Catalog (BDC) allows to integrate business data from external business applications into SharePoint application. BDC has built-in support for displaying data from such data sources as databases and web services. In other words, if the business application is a database or comprises a web service to emit data, its data can be easily incorporated into SharePoint application. If some business application isn’t database and doesn’t have any proper web services, you always can develop your own web service to provide access to the business application’s data.

Once the external business application has been registered within Business Data Catalog, its data can be used in Business Data Web Parts, Business Data Column, Search, User profile and other custom solutions.

BDC provides access to the underlying data sources with a declarative Metadata Model. The Metadata Model allows to describe a simplified and consistent client object model over any business application. What is Metadata? Metadata describes the business applications’ APIs. For each business application, Metadata defines the business entities that the business application interacts with and the methods available in the business application. The Business Data Catalog stores the metadata in the metadata repository. So, using the Metadata Model, developer describes the API of the business application in a xml-file, so called Application Definition File. Then SharePoint administrator imports the Application Definition into the Business Data Catalog to register Metadata the file contains. After that the data from business application becomes available. The schematic structure of Application Definition File is shown below:

<LobSystem Type="..." Name="...">
  <LobSystemInstances>
    <LobSystemInstance Name="...">
      <Properties>
        ...
      </Properties>
    </LobSystemInstance>
  </LobSystemInstances>
  <Entities>
    <Entity Name="...">
      <Properties>
        ...
      </Properties>
      <Identifiers>
        ...
      </Identifiers>
      <Methods>
        ...
      </Methods>
    </Entity>
  </Entities>
</LobSystem>

*Note: some attributes are skipped

The LobSystem is a container for all of the other objects in the Metadata object model, and the root node in the Application Definition File. Each LobSystem object has an unique name and is of a certain type: either Database or Web Service. LobSystem object contains LobSystemInstances and Entities. The LobSystemInstance contains properties that define the authentication of the connection and the provider, which is used to connect to the external data source. Entity defines a type of returned business data object, it contains identifiers, methods and actions. Also Entity can have other related entities associated with them.

In terms of SharePoint Central Administration, the LobSystem is a Business Data Catalog Application or BDC Application, while LobSystemInstance can be named Business Data Catalog Application Instance or BDC Application Instance. Entity in SharePoint Central Administration and in Metadata Model means the same.

BDC Metadata Model Schema

All BDC Applications registered in Business Data Catalog can be viewed through the Central Administration: open SharePoint 3.0 Central Administration, click the name of Shared Service Provider (in my case it’s SharedServices1), then click the View applications link in the Business Data Catalog section. To create Application Definition File and import it to the Business Data Catalog, read the very good article written by Tobias Zimmergren.

To work with BDC programmatically you should use types from the following namespaces:

using Microsoft.Office.Server;
using Microsoft.Office.Server.ApplicationRegistry.MetadataModel;
using Microsoft.Office.Server.ApplicationRegistry.Infrastructure;
using Microsoft.Office.Server.ApplicationRegistry.Runtime;

You mainly will use the next interfaces and objects:

namespace Microsoft.Office.Server.ApplicationRegistry.MetadataModel
{
    // Provides access to all of the LOB systems and LOB system instances registered in the Business Data Catalog
    public sealed class ApplicationRegistry { ... }

    // Represents a business application registered in the Business Data Catalog
    public class LobSystem : MetadataObject { ... }

    // Represents an instance of a business application registered in the Business Data Catalog
    public class LobSystemInstance : MetadataObject { ... }

    // Represents a type of returned business data object, contains identifiers, methods and actions
    public class Entity : DataClass { ... }
}

namespace Microsoft.Office.Server.ApplicationRegistry.Runtime
{
    // Represents a filter that limits the instances returned to those that meet the comparison operator condition
    public class ComparisonFilter : UserInputFilter { ... }

    // Represents a filter that limits the instances returned to those where field like value, where value may contain the asterisk (*) wildcard character
    public class WildcardFilter : ComparisonFilter { ... }

    // Represents instances of business objects
    public interface IEntityInstance : IInstance { ... }

    // Provides a single iteration over the entity instance collection
    public interface IEntityInstanceEnumerator : IEnumerator<IEntityInstance> { ... }
}

namespace Microsoft.Office.Server.ApplicationRegistry.Infrastructure
{
    // Provides encoding and decoding of entity instance identifiers
    public static class EntityInstanceIdEncoder { ... }

    // Represents the SQL session provider to connect to the Shared Services Provider database
    public sealed class SqlSessionProvider { ... }
}

The Business Data Catalog is implemented as a Microsoft Office SharePoint Server 2007 Shared Service. If you are going to deal with BDC inside a standalone window/console-based application or inside SharePoint Jobs, you have to prepare Shared Resource Provider to use in the Object Model. For that, you need to invoke the method SqlSessionProvider.Instance().SetSharedResourceProviderToUse:

namespace Microsoft.Office.Server.ApplicationRegistry.Infrastructure
{
    public sealed class SqlSessionProvider
    {
        // some methods are omitted
        public void SetSharedResourceProviderToUse(string sharedResourceProviderName);
        public void SetSharedResourceProviderToUse(ServerContext serverContext);        
    }
}

The first variant of the method accepts a name of Shared Resource Provider. In terms of SharePoint Central Administration, the method requires the name of Shared Service. You can see all deployed Shared Services through the Central Administration: open SharePoint 3.0 Central Administration, find the Shared Services Administration section inside the left-side navigation bar and look through the available Shared Services. One of them is a default Shared Service. Another way is to get programmatically the name of Shared Service, which serves your web application. To learn more, please read the following article – How to get Shared Service name.

The second variant of the method use an instance of ServerContext. Below is two auxiliary methods I usually use as wrappers to SetSharedResourceProviderToUse:

public static void PrepareSharedServices(SPSite spSite)
{
    ServerContext sc = ServerContext.GetContext(spSite);
    SqlSessionProvider.Instance().SetSharedResourceProviderToUse(sc);
}

public static void PrepareSharedServices(string sharedServicesName)
{
    SqlSessionProvider.Instance().SetSharedResourceProviderToUse(sharedServicesName);    
}

Do not use this method inside your SharePoint web application, otherwise an exception will be thrown. Because in web context the Business Data Catalog uses the default Shared Services Provider automatically. As I said above, use SetSharedResourceProviderToUse only inside standalone non-web applications or SP Jobs.

SharePoint: How to add Content Type programmatically

September 16th, 2011 No comments

     To add a new Content Type through the SharePoint Object Model I’ve developed a few auxiliary methods and classes. The main class is ContentType class. It contains information used for specifying the required properties of the SPContentType object being added.

public class ContentType
{
    public string         ParentContentTypeName { get; set; }
    public string         Name                  { get; set; }
    public List<FieldRef> FieldRefs             { get; set; }

    public ContentTypeFormUrls   FormUrls       { get; set; }
    public ContentTypeProperties Properties     { get; set; }
    public SPContentTypeId       Id             { get; set; }
}

The FieldRefs property represents a collection of fields that have to be added to the Content Type. The other property names are self-descriptive.

The ContentType class makes reference to classes like FieldRef, ContentTypeFormUrls and ContentTypeProperties.

public class FieldRef
{
    public Guid?  ID                 { get; set; }
    public string Name               { get; set; }
    public string DisplayName        { get; set; }
    public bool?  IsRequired         { get; set; }
    public bool?  IsHidden           { get; set; }
    public bool?  IsReadOnly         { get; set; }

    public string Aggregation        { get; set; }
    public string Customization      { get; set; }
    public string PIAttribute        { get; set; }
    public string PITarget           { get; set; }
    public string PrimaryPIAttribute { get; set; }
    public string PrimaryPITarget    { get; set; }
    public string Node               { get; set; }
}

public class ContentTypeFormUrls
{
    public string DisplayFormUrl { get; set; }
    public string NewFormUrl     { get; set; }
    public string EditFormUrl    { get; set; }
}

public class ContentTypeProperties
{    
    public string Description                 { get; set; }
    public string Group                       { get; set; }
    public bool?  IsHidden                    { get; set; }
    public bool?  IsReadOnly                  { get; set; }
    public string NewDocumentControl          { get; set; }
    public bool?  RequireClientRenderingOnNew { get; set; }
    public bool?  IsSealed                    { get; set; }
}

The main method is AddContentType:

public static void AddContentType(SPWeb spWeb, SPList spList, ContentType contentType)
{
    // get appropriate parent content type
    SPContentType parentContentType = spWeb.AvailableContentTypes[contentType.ParentContentTypeName];
    if (parentContentType != null)
    {
        if (spList.ContentTypes[contentType.Name] == null)
        {
            spList.ContentTypesEnabled = true;

            // create new content type
            SPContentType spContentType = new SPContentType(parentContentType, spList.ContentTypes, contentType.Name);
            // set content type id
            SetContentTypeId(spContentType, contentType.Id);
            // set content type properties
            SetContentTypeProperties(spContentType, contentType.Properties);

            // add fields to conent type
            foreach (FieldRef fieldRef in contentType.FieldRefs)
            {
                if (spList.Fields.ContainsField(fieldRef.Name))
                {
                    SPField targetField = fieldRef.ID.HasValue ? spList.Fields[fieldRef.ID.Value] : spList.Fields.GetFieldByInternalName(fieldRef.Name);
                    if (targetField != null && !spContentType.Fields.ContainsField(fieldRef.Name))
                    {
                        SPFieldLink fLink = new SPFieldLink(targetField);
                        SetFieldRefProperties(fLink, fieldRef);
                        spContentType.FieldLinks.Add(fLink);
                    }
                }
                else
                    Console.Write(string.Format("Couldn't find field {0} in the list {1}", fieldRef.Name, spList.Title));
            }

            // set content type form urls
            SetContentTypeFormUrls(spContentType, contentType.FormUrls);

            // save changes
            spList.ContentTypes.Add(spContentType);
            spContentType.Update();
            spList.Update();
        }
        else
            Console.Write(string.Format("Content type {0} already exists in the list {1}", contentType.Name, spList.Title));
    }
    else
        Console.Write(string.Format("Couldn't find the parent content type {0}", contentType.ParentContentTypeName));
}

It utilizes SetContentTypeFormUrls, SetContentTypeProperties, SetFieldRefProperties and SetContentTypeId (you can read about the last method in detail in my other post – How to set Id to just created SPContentType).

public static void SetContentTypeFormUrls(SPContentType spContentType, ContentTypeFormUrls contentTypeFormUrls)
{
    if (contentTypeFormUrls == null)
        return;

    if (contentTypeFormUrls.NewFormUrl != null)
        spContentType.NewFormUrl = contentTypeFormUrls.NewFormUrl;

    if (contentTypeFormUrls.DisplayFormUrl != null)
        spContentType.DisplayFormUrl = contentTypeFormUrls.DisplayFormUrl;

    if (contentTypeFormUrls.EditFormUrl != null)
        spContentType.EditFormUrl = contentTypeFormUrls.EditFormUrl;
}

public static void SetContentTypeProperties(SPContentType spContentType, ContentTypeProperties cntProps)
{
    if (cntProps == null)
        return;

    if (cntProps.Description != null)
        spContentType.Description = cntProps.Description;
    if (cntProps.Group != null)
        spContentType.Group = cntProps.Group;
    if (cntProps.IsHidden != null)
        spContentType.Hidden = cntProps.IsHidden.Value;
    if (cntProps.IsReadOnly != null)
        spContentType.ReadOnly = cntProps.IsReadOnly.Value;
    if (cntProps.IsSealed != null)
        spContentType.Sealed = cntProps.IsSealed.Value;
    if (cntProps.NewDocumentControl != null)
        spContentType.NewDocumentControl = cntProps.NewDocumentControl;
    if (cntProps.RequireClientRenderingOnNew != null)
        spContentType.RequireClientRenderingOnNew = cntProps.RequireClientRenderingOnNew.Value;
}

public static void SetContentTypeId(SPContentType spContentType, SPContentTypeId contentTypeId)
{
    try
    {
        FieldInfo fi = typeof(SPContentType).GetField("m_id", BindingFlags.NonPublic | BindingFlags.Instance);
        if (fi != null)
            fi.SetValue(spContentType, contentTypeId);
        else
            Console.Write("Couldn't set content type id!");
    }
    catch(Exception ex)
    {
        Console.Write(string.Format("Couldn't set content type id! {0}", ex.Message));
    }
}

public static void SetFieldRefProperties(SPFieldLink fLink, FieldRef fieldRef)
{
    if (fieldRef == null)
        return;

    if (fieldRef.DisplayName != null)
        fLink.DisplayName = fieldRef.DisplayName;
    if (fieldRef.IsHidden != null)
        fLink.Hidden = fieldRef.IsHidden.Value;
    if (fieldRef.IsRequired != null)
        fLink.Required = fieldRef.IsRequired.Value;
    if (fieldRef.IsReadOnly != null)
        fLink.ReadOnly = fieldRef.IsReadOnly.Value;

    if (fieldRef.Aggregation != null)
        fLink.AggregationFunction = fieldRef.Aggregation;
    if (fieldRef.Customization != null)
        fLink.Customization = fieldRef.Customization;
    if (fieldRef.PIAttribute != null)
        fLink.PIAttribute = fieldRef.PIAttribute;
    if (fieldRef.PITarget != null)
        fLink.PITarget = fieldRef.PITarget;
    if (fieldRef.PrimaryPIAttribute != null)
        fLink.PrimaryPIAttribute = fieldRef.PrimaryPIAttribute;
    if (fieldRef.PrimaryPITarget != null)
        fLink.PrimaryPITarget = fieldRef.PrimaryPITarget;
    if (fieldRef.Node != null)
        fLink.XPath = fieldRef.Node;
}

Here is how you can use all of that stuff:

public void AddSomeContentType(SPWeb spWeb, SPList spList)
{
    ContentType newContentType = new ContentType()
    {
        ParentContentTypeName = "Item",

        Id   = new SPContentTypeId("0x0100078C8B39671A4532AB9C5AB6DCB388A6"), // id has to correspond to the parent content type
        Name = "some new content type name",

        Properties = new ContentTypeProperties() { Description = "super modern content type", Group = "List Content Types" },
        FormUrls   = new ContentTypeFormUrls() { DisplayFormUrl = "Lists/your list name/your custom page name.aspx" },
        FieldRefs  = new List<FieldRef>() 
            { 
                new FieldRef() { Name = "Title",  ID = new Guid("{fa564e0f-0b71-4ab7-b863-0177e6ddd247}"), IsRequired = false, IsReadOnly = true, DisplayName = "List Item Title" },
                new FieldRef() { Name = "Status", IsRequired = true, DisplayName = "List Item Status" },
                new FieldRef() { Name = "Involved_User", ID = new Guid("869963ef-9ca3-4ad7-a5f0-8fff724a6877"), DisplayName = "User Involved" } 
            }
    };

    AddContentType(spWeb, spList, newContentType);
}

Please note that you don’t have to fill out all properties of the auxiliary classes (ContentTypeProperties, ContentTypeFormUrls and FieldRef); the unspecified properties will be merely ignored, and the appropriate properties of SPContentType object will be left with their default values. Als,o pay attention to the fact that Content Type identifier has to contain the id of parent Content Type. Find out how to build a Content Type identifier recursively here.