Custom Data Provider

Simple Data Provider

MatchPoint data providers act as a data source for the Data Grid-, the Composite- and the Chart Web Part. A data provider defines what kind of data should be displayed in these web parts. For instance the List Item Data Provider returns items located in a SharePoint list. The page Data Providers provides a complete list of out-of-the-box data providers.

In this post I'll cover the basic aspects of a custom data provider implementation.

Our custom data provider should return a configurable list of strings. Of course that's no real life scenario, but should be enough to demonstrate the basics.

To create a data provider two classes are required:

  • A configuration class that defines the settings of the data provider.
  • A data provider instance class which contains the logic required to aggregate and return the data.

The configuration class has to inherit from the type Colygon.MatchPoint.Core.DataProviders.BaseDataProvider and the instance class from the type Colygon.MatchPoint.Core.DataProviders.BaseDataProviderInstance:

[Serializable]
public class SimpleDataProvider: BaseDataProvider {}
public class SimpleDataProviderInstance: BaseDataProviderInstance {}

We start with the implementation of the SimpleDataProvider class. In this class we define the configuration for our data provider. Since we only want to configure a list of strings all we have to do is to define a public field of type string[]:

[Serializable]
public class SimpleDataProvider: BaseDataProvider
{
  [MemberDescriptor("Each line represents an item in the result set.")]
  public string[] List;
}

The base class BaseDataProvider wants use to override the abstract method CreateInstance. We just have to return an instance of the SimpleDataProviderInstance type:

[Serializable]
public class SimpleDataProvider: BaseDataProvider
{
    [MemberDescriptor("Each line represents an item in the result set.")]
    public string[] List;
    public override BaseDataProviderInstance CreateInstance(IEnumerable columnNames)
    {
        return new SimpleDataProviderInstance(this, columnNames);
    }
}

That's it for the "configuration" part. Let's move to the "logic" part. The BaseDataProviderInstance comes with an abstract method called GetInternalData. This is where you add the custom logic and return the data rows. We override this method in order to return our strings. To gain access to the configuration we store the SimpleDataProvider in a private field:

public class SimpleDataProviderInstance : BaseDataProviderInstance
{
    private readonly SimpleDataProvider provider;
    public SimpleDataProviderInstance(SimpleDataProvider provider, IEnumerable columnNames): base(provider, columnNames)
    {
        this.provider = provider;
    }
    protected override IEnumerable<object> GetInternalData()
    {
        return provider.List;
    }
    protected override CachePolicy CachePolicy
    {
        get { return new CachePolicy(CacheGranularity.NoCache, 0); }
    }
}

That's all what is required to create a basic configurable custom data provider with MatchPoint for SharePoint.

In order to use this data provider the assembly containing the code has to be registered in the MatchPoint configuration file in the property "ExternalAssemblies".

Web Data Provider

Overview

This chapter will cover more advanced scenarios for custom MatchPoint data providers such as:

  • Advanced configuration possibilities
  • Data binding
  • Caching
  • Paging
  • Support for MatchPoint conditions
  • Support for context menus in Data Grid Web Parts
  • Column name suggestion

Theory

In this second part I'd like to explain how the data binding in the Data Grid Web Part is done. Data binding describes the link between the data source (your custom data provider) and the data view (the Data Grid Web Part). In other words, how MatchPoint extracts the value corresponding to the DataField specified in the column configuration.

When implementing a custom data provider you have to override the method IEnumerable<object> GetInteralData(). Each object in the collection represents a row in the result set. Let's say you have a SimpleColumn configuration with "Title" as DataField. When extracting the value from the row, MatchPoint ...

  1. ... checks if there is a public field or property named "Title".
  2. ... checks if the row is of type IResultRecord and uses it's indexed property (resultRecord["Title"]) to return the value.
  3. ... uses the Expression Engine as fallback by executing the expression DataItem.Title on the row.

Important to note here is the order of the extraction. For example if there is already a public property named "Title" MatchPoint won't extract the value via the indexed property.

As you can see you can basically return any type in your custom data provider and the Data Grid Web Part is able tho extract the column values. You could for instance return a collection of SharePoint list items and the SPListItem's properties and field values will be available in the Data Grid Web Part.

Hands On

Lets take the following scenario: How to return a filtered list of sub-webs in an Expression Data Provider. There are two "problems" with this approach:

  1. The SPWeb objects returned by SPWeb.Webs have to be disposed. This cannot be achieved in a MatchPoint expression. Not disposing these objects could lead to a potential memory leak.
  2. The MatchPoint expression engine does not support object-to-LINQ style filtering in the current version (3.0).

So what do we do? We write our custom Web Data Provider!

We start with the configuration class for the data provider. Since we want to filter the webs by title we need to specify an expression where the filter value comes from. If this expression points to a TextField via the connection framework, we can build a find-on-type behavior. Additionally, it might be useful to let the user specify the parent web url:

[Serializable]
public class WebDataProvider : BaseDataProvider
{
  [MemberDescriptor("Specifies an expression that returns a value which should be used to filter the web by title.")]
  public ExpressionString TitleFilterExpression;
  [MemberDescriptor("Specifies the URL of the parent web. If left empty, the current web will be used as parent.")]
  [SPUrlEditorBehavior(SPUrlType.Web)]
  [CustomEditor(typeof(SPUrlEditor))]
  public string ParentWebUrl;
  // ...
}

The attributes [CustomEditor(typeof(SPUrlEditor))] and [SPUrlEditorBehavior(SPUrlType.Web)] tell the configuation editor to display an URL picker control.

Next comes the data provider instance. We need to override the method GetInternalData() and return our webs there. Since we have to dispose the web objects returned by SPWeb.Webs and we don't want to return a disposed instance we create a helper class that holds the required web information:

public class WebInfo: IResultRecord
{
  public readonly string Title;
  public readonly string Url;
  private IDictionary allProperties;
  public object this[string name]
  {
    get { return allProperties[name]; }
  }
  public WebInfo(string title, string url, IDictionary allProperties)
  {
    Title = title;
    Url = url;
    allProperties = allProperties;
  }
}

Since we do implement the Colygon.MatchPoint.Core.DataProviders.IResultRecord interface we enable one to create a data grid column that displays properties of a web.

In the next code block we retrieve the webs with correct disposing handling. If there is a filter expression set we apply a "starts-with filter" on the web title with the filter value.

private IEnumerable GetWebs()
{
  // SPHelper.OpenWeb() will open the current web
  // if provider.ParentWebUrl is null or empty.
  using (SPWeb parent = SPHelper.OpenWeb(provider.ParentWebUrl))
  {
    foreach (SPWeb child in parent.Webs)
    {
      // We do not return the SPWeb object here since
      // we have to dispose elements of the SPWebCollection (SPWeb.Webs)
      using (child)
      {
        yield return new WebInfo(child.Title, child.Url, child.AllProperties);
      }
    }
  }
}
protected override IEnumerable<object> GetInternalData()
{
  IEnumerable webs = GetWebs();
  // If there is no filter expression defined just return the webs.
  if (provider.TitleFilterExpression == null) return webs.ToArray();
  // Evaluate the filter expression to string and filter the webs
  // by it's title.
  string filter = provider.TitleFilterExpression.EvaluateToString(null);
  return webs
    .Where(wi => wi.Title.StartsWith(filter, StringComparison.CurrentCultureIgnoreCase))
    .ToArray();
}

In order to support find-on-type behavior for our data provider we need to tell the connection framework that we are a data consumer. For this purpose we override the AddConnectionDependencies method on the data provider instance and add the TitleFilterExpression to the DependencyCollection.

public override void AddConnectionDependencies(DependencyCollection dependencies)
{
  base.AddConnectionDependencies(dependencies);
  dependencies.AddFromExpression(provider.TitleFilterExpression);
}

By registering our data provider as consumer the data grid will be automatically refreshed if the connected control changes.

results matching ""

    No results matching ""