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 ...
- ... checks if there is a public field or property named "Title".
- ... checks if the row is of type
IResultRecord
and uses it's indexed property (resultRecord["Title"]
) to return the value. - ... 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:
- The
SPWeb
objects returned bySPWeb.Webs
have to be disposed. This cannot be achieved in a MatchPoint expression. Not disposing these objects could lead to a potential memory leak. - 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.