Gunnar Peipman's ASP.NET blog

ASP.NET, C#, SharePoint, SQL Server and general software development topics.

Sponsors

News

 
 
 
DZone MVB

Links

Social

November 2007 - Posts

How to connect Business Data Catalog (BDC) lists to other SharePoint lists

Recently I had to create Web Part connection between BDC based list and usual SharePoint list. BDC list was provider and usual SharePoint list was consumer. I was pretty surprised if found out that I was able to connect BDC based list only to other BDC based Web Parts. All the other Web Parts were not able to be consumers for them. Same time BDC lists were able to be consumers of the other web parts.

IEntityInstanceProvider 

This time Google wasn't my help and documentation. Also MSDN wasn't able to help me - there were documents for everything but no texts or explanations about how and what I can use. Here is the example of perfect documentation. After hours of investigating and experimenting and hacking everything started working. As a result I created a web part that reads data from BDC list and provides it to other Web Parts.

Wrapper Web Part

The Web Part I created works as proxy between BDC lists and usual SharePoint lists. If you need common solution you can easily extend the code I will give here. Schematically works my wrapper like this.

Wrapper Web Part

 

Business Data Catalog consumer

Let's start with the darkest part of this journey - BDC consumer and its completely undocumented API. There are some things I want to say before we go and look at the code. Number one thing is - I am not 100% sure if this solution is 100% correct. But this is the way I got things to run.

In the constructor of Web Part I will initialize DataTable where we hold the value that provider part of our Web Part provides. If there is no connection from BDC list then we will provide default value to consumers of our Web Part. Consumer part of our Web Part follows ASP.NET Web Part consumers and providers way. We will define method SetConnectionPoint and accept IEntityInstanceProvider as its argument. Through this interface we are able to find out what object was selected from BDC list.

Now, here is the code.

BDC Consumer part of Web Part

using System;
using System.Collections;
using System.Data;
using System.Runtime.InteropServices;
using System.Security;
using System.Web.UI;
using AspWebParts = System.Web.UI.WebControls.WebParts;

using MdModel = Microsoft.Office.Server.ApplicationRegistry.MetadataModel;
using Microsoft.Office.Server.ApplicationRegistry.Runtime;
using Microsoft.SharePoint.Portal.WebControls;
using SPWebParts = Microsoft.SharePoint.WebPartPages;
using Microsoft.SharePoint.WebPartPages.Communication;
using Microsoft.SharePoint;

namespace MyWebPart
{
    [Guid("YOUR-GUID-HERE")]
    [Obsolete()]
    public class BDCWrapper : SPWebParts.WebPart, IRowProvider
    {
        private string serviceCaseId = string.Empty;
        private string defaultValue = string.Empty;
        private string filterField = string.Empty;
        private DataTable filterTable;
 
        /// <summary>
        /// Constructor of class. Let's initialize data table that
        /// holds filter value.
        /// </summary>
        public BDCWrapper()
        {
            this.filterTable = new DataTable();

            DataColumn column = new DataColumn();
            column.DataType = System.Type.GetType("System.string");
            column.ColumnName = this.filterField;
            column.Caption = this.filterField;
            this.filterTable.Columns.Add(column);

            DataRow dataRow = filterTable.NewRow();
            dataRow[this.filterField] = this.defaultValue;
            filterTable.Rows.Add(dataRow);
        }

        /// <summary>
        /// Render Web Part. Write out filter field's current value.
        /// </summary>

        protected override void Render(HtmlTextWriter writer)
        {
            writer.Write(this.filterField);
            writer.Write("=");
            writer.Write(this.filterTable.Rows[0][this.filterField]);
        }

        /// <summary>
        /// Connection point for Business Data Catalog Web Parts.
        /// </summary>
        /// <param name="provider">Connecting BDC provider.</param>
        /// <remarks>
        /// Keep the ID (currently Cons2193838) and don't delete it or
        /// you may face somenasty problems. You can always change
        /// this ID.
        /// </remarks>
        [AspWebParts.ConnectionConsumer("BDCConsumer", "Cons2193838")]
        public void SetConnectionPoint(IEntityInstanceProvider provider)
        {
            // Method is called but there is no provider.
            if (provider == null) return;

            // Method is called but there is no selected entity.
            MdModel.Entity ent = provider.SelectedConsumerEntity;
            if(ent==null) return;

            // Get a view we have to use to ask selected entity.
            MdModel.View view = ent.GetSpecificFinderView();
            if (view == null) return;
           
            // Now we have view, let's ask entity instance.
            // NB! View cannot be null!
            IEntityInstance inst = provider.GetEntityInstance(view);
            if(inst == null) return;

            // Let's ask entity as data table.
            DataTable dt = inst.EntityAsDataTable;
            if(dt==null) return;

            // Check if data table has filter field.           
            if (dt.Columns.IndexOf(this.filterField) <0) return;

            // Check if data table has rows.
            if (dt.Rows.Count == 0) return;

            // Everything is okay, let's save filter value;
            this.serviceCaseId = dt.Rows[0][this.filterField].ToString();
        }
    }
}

Of course, for production code you should have here also lines of code for logging anomalies. Also some error handling will be fine. I removed this code because otherwise it is too long to show here.

Consumer part of Web Part

The other part of wrapper works as row provider and is understandable to all usual SharePoint lists. I had some troubles getting it work using IWebPartRow, so I stayed on older and unfortunately deprecated IRowProvider interface. The last one of them is very well documented, by the way. I think this part of my code doesn't need further explanations, so here it is.

NB! Add this code to the end of class given below.


/// <summary>
/// Provider initialization event.
/// </summary>
[Obsolete]
public event RowProviderInitEventHandler RowProviderInit;

/// <summary>
/// Provider is ready to provide data.
/// </summary>
[Obsolete]
public event RowReadyEventHandler RowReady;

private bool connected = false;
private string connectedWebPartTitle = string.Empty;
private string consumerMsg = string.Empty;

/// <summary>
/// Register provider interfaces.
/// </summary>
[Obsolete]
public override void EnsureInterfaces()
{
    try
    {
        RegisterInterface("MyRowProviderInterface",
            InterfaceTypes.IRowProvider,
            SPWebParts.WebPart.UnlimitedConnections,
            ConnectionRunAt.Server,
            this,
            "",
            "Provide filter to",
            "Provides a row to a consumer Web Part.",
            true);
    }
    catch (SecurityException ex)
    {
        this.consumerMsg = ex.ToString();
        registrationErrorOccurred = true;
    }
}

/// <summary>
/// Web Part can run only on server.
/// </summary>
[Obsolete]
public override ConnectionRunAt CanRunAt()
{
    return ConnectionRunAt.Server;
}

/// <summary>
/// Web Part starts connecting.
/// </summary>
[Obsolete]
public override void PartCommunicationConnect(
    string interfaceName,
    SPWebParts.WebPart connectedPart,
    string connectedInterfaceName,
    ConnectionRunAt runAt)
{
    if (interfaceName != "MyRowProviderInterface") return;
    this.connected = true;
}

/// <summary>
/// Initialize communication.
/// </summary>
[Obsolete]
public override void PartCommunicationInit()
{
    try
    {
        if (!this.connected) return;

        if (RowProviderInit != null)
        {
            RowProviderInitEventArgs initArgs = new RowProviderInitEventArgs();
            initArgs.FieldList = new string[] { this.filterField };
            initArgs.FieldDisplayList = new string[] { this.filterField };

            RowProviderInit(this, initArgs);
        }
    }
    catch (Exception ex)
    {
        this.consumerMsg = ex.ToString();
    }
}

/// <summary>
/// Called when provider is ready and consumer is waiting data.
/// </summary>
[Obsolete]
public override void PartCommunicationMain()
{
    if (!this.connected) return;
    if (this.RowReady == null) return;

    try
    {
        RowReadyEventArgs initArgs;
        initArgs = new RowReadyEventArgs();
        initArgs.Rows = new DataRow[] { };
        initArgs.SelectionStatus = "Standard";
        RowReady(this, initArgs);
    }
    catch (Exception ex)
    {
        this.consumerMsg = ex.ToString();
    }
}

/// <summary>
/// Returns initialization arguments.
/// </summary>
/// <param name="interfaceName">Name of interface.</param>
[Obsolete]
public override InitEventArgs GetInitEventArgs(string interfaceName)
{
    if (interfaceName != "MyRowProviderInterface") return null;

    try
    {
        EnsureChildControls();

        RowProviderInitEventArgs initArgs;
        initArgs = new RowProviderInitEventArgs();
        initArgs.FieldList = new string[] { this.filterField };
        initArgs.FieldDisplayList = new string[] { this.filterField };

        return (initArgs);
    }
    catch (Exception ex)
    {
        this.consumerMsg = ex.ToString();
        return null;
    }
}

Now should everything be done. If you want you can use attribute consumerMsg in Render method to show consuming status of wrapper Web Part.

Conclusion

Nothing is impossible if you know reflecton and you have good sense when there is no more point to struggle in search engines hoping to find an answer. If you have any comments about this code please feel free to drop me a line here. Happy connecting!

Posted: Nov 29 2007, 06:33 PM by DigiMortal | with 12 comment(s) |
Filed under: ,
C# Extension Methods

One of new cool features that will be available in C# 3.0 are extension methods. Extension methods will allow us to extend existing classes with new functionality. In this example I will show you how to extend System.string with two methods that are very popular in PHP: nl2br() and md5().

Extension methods must be defined as static methods of some static class. Now let's write the static class called StringExtensions.


using System;
using System.Security.Cryptography;
using System.Text;

namespace MyExamples
{
    static class StringExtensions
    {
        public static string Nl2Br(this string s)
        {
            return s.Replace("\r\n", "<br />").Replace("\n", "<br />");
        }

        public static string MD5(this string s)
        {
            MD5CryptoServiceProvider provider;
            provider = new MD5CryptoServiceProvider();
            byte[] bytes = Encoding.UTF8.GetBytes(s);
            StringBuilder builder = new StringBuilder();

            bytes = provider.ComputeHash(bytes);
           
            foreach (byte b in bytes)
                builder.Append(b.ToString("x2").ToLower());
           
            return builder.ToString();
        }
    }
}

Notice the argument list of these function, especially keyword this. Keyword this followed by type tells to compilator that this method is applied to specified type.

Now let's try out our new methods.


static void Main(string[] args)
{
    string s = "First\r\nSecond\nThird";
    Console.WriteLine(s.Nl2Br());
    Console.WriteLine(s.MD5());
    Console.Write("Press any key...");
    Console.ReadLine();
}

If everything is okay we will get the following output:

    First<br />Second<br />Third
    22738d44da2809b621cc6e6609152c72
    Press any key...

Pretty convenient, isn't it?

Although extension methods are very powerful feature in new C# it has to be mentioned that over-using them may lead you to very bad problems. Your code might be hard to maintain and understand for you and the other programmers. But in the case of sensible use of extension methods you will find them to be very convenient and powerful features that help you a lot.


kick it on DotNetKicks.com pimp it vote it on WebDevVote.com Progg it Shout it
C# automatic properties

C# 3.0 makes it very convenient to create properties that doesn't carry any functionalities besides returning value of attribute and assigning value to it. This new feature is called automatic properties.

Suppose we have a class with big load of properties that just return attribute values and assign given values to them. Something like you can see in following code.


public class Product
{
    private string description;
    private Int32 size;
    private decimal price;

    public string Description
    {
        get { return description; }
        set { description = value; }
    }

    public Int32 Size
    {
        get { return size; }
        set { size = value; }
    }

    public decimal Price
    {
        get { return price; }
        set { price = value; }
    }

    ...
}

All three properties given here are following the same pattern: get returns value of attribute, set assigns value to attribute. There is no more functionality in the bodies of these properties. This pattern gave an idea to language authors. Why not to use some shorter syntax for this kind of properties and let compiler to do all the dirty work? So, here is what we can use in C# 3.0.


public class Product
{
    public string Description { get; set; }
    public Int32 Size { get; set; }
    public decimal Price { get; set; }
    ...
}

Automatic properties are compiled as properties, not as public attributes of class. This way we are able to add functionality to properties later and we doesn't break connections between class and the other classes.


kick it on DotNetKicks.com pimp it Progg it Shout it
More Posts