How To Use SharePoint 2010 Secure Store As Single Sign-On Service For SAP Applications Using ERPConnect

*** NEW! My blog moved to my homepage at http://www.parago.de! ***

The Secure Store Service in SharePoint 2010 replaces the Single Sign-on Shared Service of MOSS 2007 and provides an easy way to map user credentials of external resources like SAP systems to Windows users. During the process of developing SAP interfaces using the very handy ERPConnect library from Theobald Software you have to open a R3 connection with the SAP system using SAP account credentials (username and password).

In most cases you will use a so called technical user with limited access rights to execute or query objects in SAP, but a SAP system saves a lot of sensitive data which cannot all be shown to all users. So, creating a new secure store in SharePoint 2010 to save the SAP user credentials will be the solution. Accessing the secure store from program code is quite simple.

A trail version of the ERPConnect library can be downloaded at www.theobald-software.com.

Secure Store Configuration

The Secure Store Service will be managed by the Central Administration (CA) of SharePoint 2010 under Application Management > Manage service applications > Secure Store Service:

Screenshot1

As the screenshot above shows is it possible to create multiple target applications within one Secure Store Service.

Clicking the New button will open the Create New Secure Store Target Application page. In this dialog you have to enter the new Target Application ID, a display name, a contact email address and other application related details (see screenshot below).

Screenshot2

Next, the application fields must be defined:

Screenshot3

It’s important to select the field type User Name and Password, because our implementation later on will check the target application for those two field types.

In the last dialog step the application administrator must be defined. After defining the administrator and clicking the Ok button SharePoint is creating a new secure store:

Screenshot5

Next, the Windows users must be mapped to the SAP user credentails. Therefore mark the checkbox for the newly created secure store SAPCredentialsStore and click the Set button in the toolbar. This opens the following dialog:

Screenshot6

The Credential Owner is the Windows user for whom the SAP user credentials will be set. Enter the SAP username and password and click the Ok button to save them.

That’s it !

Secure Store Programming

Accessing the secure store by code is simple. We implement a SecureStore class which will encapsulate all the access logic. A second class called SecureStoreCredentials contains the retrieved user credentials returned by the method GetCurrentCredentials of the SecureStore class.

But first, we need to create a Visual Studio 2010 SharePoint project and reference a couple of assemblies. You can directly enter the file paths in the Reference dialog (Browse tab) to add the assemblies:

Microsoft.BusinessData.dll
C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI\Microsoft.BusinessData.dll

Microsoft.Office.SecureStoreService.dll
C:\Windows\assembly\GAC_MSIL\Microsoft.Office.SecureStoreService\14.0.0.0__71e9bce111e9429c\Microsoft.Office.SecureStoreService.dll

The following code shows the SecureStore class implementation:

internal class SecureStore
{
  public string ApplicationId { get; private set; }

  public SecureStore(string applicationId)
  {
    if(string.IsNullOrEmpty(applicationId))
      throw new ArgumentNullException("applicationId");
    if(!IsApplicationValid(applicationId))
      throw new ArgumentException(string.Format("Target application with ID '{0}' is not defined.", applicationId));

    ApplicationId = applicationId;
  }

  public SecureStoreCredentials GetCurrentCredentials()
  {
    SecureStoreProvider provider = new SecureStoreProvider { Context = SPServiceContext.Current };
    string userName = string.Empty;
    string password = string.Empty;

    using(SecureStoreCredentialCollection data = provider.GetCredentials(ApplicationId))
    {
      foreach(ISecureStoreCredential c in data)
      {
        if(c != null)
        {
          if(c.CredentialType == SecureStoreCredentialType.UserName)
            userName = GetDecryptedCredentialString(c.Credential);
          else if(c.CredentialType == SecureStoreCredentialType.Password)
            password = GetDecryptedCredentialString(c.Credential);
        }
      }
    }

    if(string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
      throw new SecureStoreException("Credentials for the current Windows user are not valid or not defined.");

    return new SecureStoreCredentials(userName, password);
  }

  public static bool IsApplicationValid(string applicationId)
  {
    if(string.IsNullOrEmpty(applicationId))
      throw new ArgumentNullException("applicationId");

    SecureStoreProvider provider = new SecureStoreProvider { Context = SPServiceContext.Current };

    foreach(TargetApplication application in provider.GetTargetApplications())
    {
      if(application.ApplicationId == applicationId)
      {
        ReadOnlyCollection<ITargetApplicationField> fields = provider.GetTargetApplicationFields(applicationId);
        bool existsUserNameDefinition = false;
        bool existsPasswordDefinition = false;

        foreach(TargetApplicationField field in fields)
        {
          if(field.CredentialType == SecureStoreCredentialType.UserName)
            existsUserNameDefinition = true;
          else if(field.CredentialType == SecureStoreCredentialType.Password)
            existsPasswordDefinition = true;
        }

        if(existsUserNameDefinition && existsPasswordDefinition)
          return true;
      }
    }

    return false;
  }

  public static string GetDecryptedCredentialString(SecureString secureString)
  {
    IntPtr p = Marshal.SecureStringToBSTR(secureString);

    try
    {
      return Marshal.PtrToStringUni(p);
    }
    finally
    {
      if(p != IntPtr.Zero)
        Marshal.FreeBSTR(p);
    }
  }
}

The constructor checks if an application ID is passed and if it’s valid by calling the static method IsApplicationValid. In first place, the IsApplicationValid method is creating an instance of the SecureStoreProvider class to get access to Secure Store Service. The SecureStoreProvider class provides all methods to talk to the SharePoint service. Then, the method queries for all target applications and checks for the given application. If the application has been created and can be found, the method will analyse the application field definitions. The IsApplicationValid method then looks for two fields of type User Name and Password (see above).

The GetCurrentCredentials method is actually trying to get the SAP user credentials from the store. The method is creating an instance of the SecureStoreProvider class to get access to service and then calls the GetCredentials method of the provider class. If credentials are available the method decrypt the username and password from type SecureString using the internal method GetDecryptedCredentialString. It then will wrap and return the data into an instance of the SecureStoreCredentails class.

For more details of the implementation see the source code.

Accessing SAP Using The Secure Store Credentials

The sample and test code calls the SAP function module SD_RFC_CUSTOMER_GET to retrieve all customer data that match certain criteria (where NAME1 starts with Te*):

Screenshot7

The following code shows the implementation of the Test button click event:

protected void OnTestButtonClick(object sender, EventArgs e)
{
  string licenseKey = "<LICENSEKEY>";
  string connectionStringFormat = "CLIENT=800 LANG=EN USER={0} PASSWD={1} ASHOST=HAMLET ...";

  R3Connection connection = null;

  try
  {
    LIC.SetLic(licenseKey);

    ...

    SecureStoreCredentials credentials =
      new SecureStore(ApplicationID.Text).GetCurrentCredentials();
    string connectionstring =
      string.Format(connectionStringFormat, credentials.UserName, credentials.Password);

    connection = new R3Connection(connectionstring);
    connection.Open();

    RFCFunction function = connection.CreateFunction("SD_RFC_CUSTOMER_GET");
    function.Exports["NAME1"].ParamValue = "Te*";
    function.Execute();

    ResultGrid.DataSource = function.Tables["CUSTOMER_T"].ToADOTable();
    ResultGrid.DataBind();

    OutputLabel.Text = string.Format("The test called...",
      ApplicationID.Text, Web.CurrentUser.Name, Web.CurrentUser.LoginName,
      credentials.UserName);
  }
  catch(Exception ex)
  {
    WriteErrorMessage(ex.Message);
  }
  finally
  {
    if(connection != null && connection.Ping())
      connection.Close();
  }
}

The interesting part is the retrieval of the SAP user credentials from the secure store defined in the text box named ApplicationID. The application ID will be passed as parameter to the constructor of the SecureStore class. After creating the instance the method GetCurrentCredentials will be called to ask the store for the credentials of the current Windows user.

After the user credential query has been successfully executed the SAP connection string will be constructed. Then the connection string will then be used to create an instance of the R3Connection class to connect with the SAP system. The remaining code is just calling the function module SD_RFC_CUSTOMER_GET and binding the result to the SPGridView.

Download Source Code | Download Article (PDF)

How To Implement A Modern Progress Dialog For WPF Applications

*** NEW! My blog moved to my homepage at http://www.parago.de! ***

Developing a Windows Presentation Foundation (WPF) application requires sometimes to execute an asynchronous task with long-running execution steps, e.g. calling a web service. If those steps are triggered by user interactions, you want show a progress dialog to block the user interface and display detail step information. In some cases you also want to allow the user to stop the long-running task by clicking a Cancel button.

The following screenshots show some samples of progress dialogs implemented in this blog entry:

image

image

image

image

This article will show how to implement such progress dialogs in WPF with C#. All of the above dialogs can be used with one implementation. The solution will also show how to hide the close button of a window, which is officially not supported by WPF.

The code snippet below shows how to use and display such a progress dialog:

ProgressDialogResult result = ProgressDialog.Execute(this, "Loading data...", () => {

  // TODO: Do your work here!

});

if(result.OperationFailed)
  MessageBox.Show("ProgressDialog failed.");
else
  MessageBox.Show("ProgressDialog successfully executed.");

The ProcessDialog class provides a number of static Execute methods (overrides) to easily setup a long-running task with a dialog window. The first parameter always defines the parent window in order to center the process dialog relative to the parent window. The second parameter is the text message displayed in all dialogs. The third parameter is the asynchronous method itself.

The fourth parameter allows to pass additional settings. Those dialog settings, represented by an instance of the ProcessDialogSettings class, define if the dialog shows a sub label, has a Cancel button or displays the progress bar itself in percentage or indeterminate. Predefined settings are also defined as static properties.

Using lambda expressions in C#, it is very comfortable to start a long-running task and displaying the dialog. You can also communicate with the progress dialog to report messages to the user by calling the Report method of the ProgressDialog class:

ProgressDialogResult result = ProgressDialog.Execute(this, "Loading data...", (bw) => {

  ProgressDialog.Report(bw, "Connecting to the server...");

  // TODO: Connect to the server

  ProgressDialog.Report(bw, "Reading metadata...");

  // TODO: Reading metadata

}, ProgressDialogSettings.WithSubLabel);

The two samples above did not show a Cancel button. The next code shows a progress dialog with a Cancel button including code to check if the long-running code must be cancelled:

int millisecondsTimeout = 1500;

ProgressDialogResult result = ProgressDialog.Execute(this, "Loading data", (bw, we) => {

  for(int i = 1; i <= 5; i++)
  {
    if(ProgressDialog.ReportWithCancellationCheck(bw, we, "Executing step {0}/5...", i))
      return;

    Thread.Sleep(millisecondsTimeout);
  }

  // So this check in order to avoid default processing after the Cancel button has been
  // pressed. This call will set the Cancelled flag on the result structure.
  ProgressDialog.CheckForPendingCancellation(bw, we);

}, ProgressDialogSettings.WithSubLabelAndCancel);

if(result.Cancelled)
  MessageBox.Show("ProgressDialog cancelled.");
else if(result.OperationFailed)
  MessageBox.Show("ProgressDialog failed.");
else
  MessageBox.Show("ProgressDialog successfully executed.");

Calling the ReportWithCancellationCheck method will check for a pending cancellation request (from the UI) and will may set the Cancel property of the DoWorkEventArgs class passed from the underlying BackgroundWorker object to True. Otherwise the method will display the message and continue processing.

The Exceute method of the ProgressDialog class will return an instance of the ProgressDialogResult class that returns the status of the execution. Thrown exceptions in the task method will be stored in the Error property if the OperationFailed property is set to True. For more samples see the Visual Studio 2010 solution you can download from my homepage.

The ProgressDialog window contains the main application logic. The above described static Execute methods will internally call the ExecuteInternal method to create an instance of the ProgressDialog window passing and setting all necessary values. Then the Execute method with the asynchronous method as parameter is called:

internal static ProgressDialogResult ExecuteInternal(Window owner, string label,
  object operation, ProgressDialogSettings settings)
{
  ProgressDialog dialog = new ProgressDialog(settings);
  dialog.Owner = owner;

  if(!string.IsNullOrEmpty(label))
    dialog.Label = label;

  return dialog.Execute(operation);
}

The operation method can be a delegate of the following type:

· Action

· Action<BackgroundWorker>

· Action<BackgroundWorker, DoWorkEventArgs>

· Func<object>

· Func<BackgroundWorker, object>

· Func<BackgroundWorker, DoWorkEventArgs, object>

The Func types can return a value that will be used to set the Result property of the ProgressDialogResult class.

The Excute method is implemented as follows:

internal ProgressDialogResult Execute(object operation)
{
  if(operation == null)
    throw new ArgumentNullException("operation");

  ProgressDialogResult result = null;

  _isBusy = true;

  _worker = new BackgroundWorker();
  _worker.WorkerReportsProgress = true;
  _worker.WorkerSupportsCancellation = true;

  _worker.DoWork +=
    (s, e) => {
      if(operation is Action)
        ((Action)operation)();
      else if(operation is Action<BackgroundWorker>)
        ((Action<BackgroundWorker>)operation)(s as BackgroundWorker);
      else if(operation is Action<BackgroundWorker, DoWorkEventArgs>)
        ((Action<BackgroundWorker, DoWorkEventArgs>)operation)(s as BackgroundWorker, e);
      else if(operation is Func<object>)
        e.Result = ((Func<object>)operation)();
      else if(operation is Func<BackgroundWorker, object>)
        e.Result = ((Func<BackgroundWorker, object>)operation)(s as BackgroundWorker);
      else if(operation is Func<BackgroundWorker, DoWorkEventArgs, object>)
        e.Result = ((Func<BackgroundWorker, DoWorkEventArgs, object>)operation)(s as BackgroundWorker, e);
      else
        throw new InvalidOperationException("Operation type is not supoorted");
    };

  _worker.RunWorkerCompleted +=
    (s, e) => {
      result = new ProgressDialogResult(e);
      Dispatcher.BeginInvoke(DispatcherPriority.Send, (SendOrPostCallback)delegate {
        _isBusy = false;
        Close();
      }, null);
    };

  _worker.ProgressChanged +=
    (s, e) => {
      if(!_worker.CancellationPending)
      {
        SubLabel = (e.UserState as string) ?? string.Empty;
        ProgressBar.Value = e.ProgressPercentage;
      }
    };

  _worker.RunWorkerAsync();

  ShowDialog();

  return result;
}

The ProgressDialog class is using internally a BackgroundWorker object to handle the asynchronous execution of the task or operation method.

For more details of the implementation see the source code.

Download Source Code

Download Article (PDF)

UPDATE:

I wrote a second edition of the progress dialog which is using a ProgressDialogContext class to access BackgroundWorker and the DoWorkEventArgs. Also, all Report methods moved to the context class. So, you can basically use the static ProgressDialog.Current property within the asynchronous method and its sub methods to report to the progress dialog. This usefully if you call a chain of methods in which you have to report messages to the dialog and may need to cancel the process.

Download Source Code with ProgresDialogContext class

How To Implement A Custom SharePoint 2010 Logging Service For ULS And Windows Event Log

Prior to Microsoft SharePoint 2010 there was no official documented way to programmatically use the built-in ULS service (Unified Logging Service) to log own custom messages. There are still solutions available on the internet that can be used, but SharePoint 2010 now supports full-blown logging service support.

To log a message to the SharePoint log files just call the WriteTrace method of the SPDiagnosticsService class:

SPDiagnosticsService.Local.WriteTrace(0, new SPDiagnosticsCategory("My Category", TraceSeverity.Medium, EventSeverity.Information), TraceSeverity.Medium, "My log message");

The problem with this technique is that the log entry does not contain any category information, instead SharePoint uses the value "Unknown":

image

Of course, that does not matter if someone develops small solutions. Developing custom solutions you may want to implement an own logging service with custom categories and UI integration within the Central Administration (CA) of SharePoint 2010.

This article shows how to develop a custom logging service that integrates with the Diagnostic Logging UI of the Central Administration:

image

Custom Logging Service

A custom logging service must inherit from the SPDiagnosticsServiceBase class. This is the base class for diagnostic services in SharePoint and it offers the option to log messages to log files via ULS and to the Windows Event Log. By overriding the ProvideAreas method the service provides information about diagnostic areas, categories and logging levels. A diagnostic area is a logical container of one or more categories.

The sample service defines one diagnostic area and one category (used by application pages) for this area:

[Guid("D64DEDE4-3D1D-42CC-AF40-DB09F0DFA309")] 
public class LoggingService : SPDiagnosticsServiceBase
{
  public static class Categories
  {
    public static string ApplicationPages = "Application Pages";
  }

  public static LoggingService Local
  {
    get { return SPFarm.Local.Services.GetValue(DefaultName); }
  }

  public static string DefaultName
  {
    get { return "Parago Logging Service"; }
  }

  public static string AreaName
  {
    get { return "Parago"; }
  }

  protected override IEnumerable ProvideAreas()
  {
    List areas = new List
    {
      new SPDiagnosticsArea(AreaName, 0, 0, false, new List
      {
        new SPDiagnosticsCategory(Categories.ApplicationPages, null, TraceSeverity.Medium, 
              EventSeverity.Information, 0, 0, false, true)
      })
    };

    return areas;
  }

  // . . .

}

The area name as well as the category names will be also shown in the Diagnostic Logging UI of the CA. It is also possible to define a resource DLL to localize the names.

The service will offer the two static methods WriteTrace and WriteEvent. WriteTrace writes the log message to the SharePoint log files, usually saved in the SharePoint folder C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\LOGS.

The WriteEvent writes the log message to the Windows Event Log. The event source is called like the AreaName and later on created within the FeatureReceiver:

[Guid("D64DEDE4-3D1D-42CC-AF40-DB09F0DFA309")] 
public class LoggingService : SPDiagnosticsServiceBase
{

  ...

  public static void WriteTrace(string categoryName, TraceSeverity traceSeverity, 
    string message)
  {
    if(string.IsNullOrEmpty(message))
      return;

    try
    {
      LoggingService service = Local;

      if(service != null)
      {
        SPDiagnosticsCategory category = service.Areas[AreaName].Categories[categoryName];
        service.WriteTrace(1, category, traceSeverity, message);
      }
    }
    catch { }
  }

  public static void WriteEvent(string categoryName, EventSeverity eventSeverity, 
    string message)
  {
    if(string.IsNullOrEmpty(message))
      return;

    try
    {
      LoggingService service = Local;

      if(service != null)
      {
        SPDiagnosticsCategory category = service.Areas[AreaName].Categories[categoryName];
        service.WriteEvent(1, category, eventSeverity, message);
      }
    }
    catch { }
  }
}

The usage of the new custom logging service is quite simple:

 

// ULS Logging
LoggingService.WriteTrace(LoggingService.Categories.ApplicationPages, 
  TraceSeverity.Medium, "...");

// Windows Event Log
LoggingService.WriteEvent(LoggingService.Categories.ApplicationPages, 
  EventSeverity.Information, "...");

Next, we need to register it with SharePoint.

Service Registration

The custom logging service must be registered with SharePoint 2010 to show up in the Diagnostic Logging UI of the CA. The event sources also must be created on each server of the SharePoint farm. These two registration steps can be bundle within the FeatureActivated override method of the FeatureReceiver.

[Guid("50CA5F69-381F-4C2A-BE6C-F28219AFF20C")]
public class FeatureEventReceiver : SPFeatureReceiver
{
  const string EventLogApplicationRegistryKeyPath = 
    @"SYSTEM\CurrentControlSet\services\eventlog\Application";

  public override void FeatureActivated(SPFeatureReceiverProperties properties)
  {
    RegisterLoggingService(properties);
  } 

  public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  {
    UnRegisterLoggingService(properties);
  }

  static void RegisterLoggingService(SPFeatureReceiverProperties properties)
  {
    SPFarm farm = properties.Definition.Farm;

    if(farm != null)
    {
      LoggingService service = LoggingService.Local;

      if(service == null)
      {
        service = new LoggingService();
        service.Update();

        if(service.Status != SPObjectStatus.Online)
          service.Provision();
      }

      foreach(SPServer server in farm.Servers)
      {
        RegistryKey baseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, 
                                server.Address);

        if(baseKey != null)
        {
          RegistryKey eventLogKey = baseKey.OpenSubKey(EventLogApplicationRegistryKeyPath,  
                                      true);

          if(eventLogKey != null)
          {
            RegistryKey loggingServiceKey = eventLogKey.OpenSubKey(LoggingService.AreaName);

            if(loggingServiceKey == null)
            {
              loggingServiceKey = eventLogKey.CreateSubKey(LoggingService.AreaName, 
                                   RegistryKeyPermissionCheck.ReadWriteSubTree);
              loggingServiceKey.SetValue("EventMessageFile", 
                @"C:\Windows\Microsoft.NET\Framework\v2.0.50727\EventLogMessages.dll", 
                RegistryValueKind.String);
            }
          }
        }
      }
    }
  }

  static void UnRegisterLoggingService(SPFeatureReceiverProperties properties)
  {
    SPFarm farm = properties.Definition.Farm;

    if(farm != null)
    {
      LoggingService service = LoggingService.Local;

      if(service != null)
        service.Delete();

      foreach(SPServer server in farm.Servers)
      {
        RegistryKey baseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, 
                                server.Address);

        if(baseKey != null)
        {
          RegistryKey eventLogKey = baseKey.OpenSubKey(EventLogApplicationRegistryKeyPath, 
                                      true);

          if(eventLogKey != null)
          {
            RegistryKey loggingServiceKey = eventLogKey.OpenSubKey(LoggingService.AreaName);

            if(loggingServiceKey != null)
              eventLogKey.DeleteSubKey(LoggingService.AreaName);
          }
        }
      }
    }
  }
}

The FeatureActivated override is calling the RegisterLoggingService helper method to register with the SharePoint system if the service is not already available. Since one of the base classes of LoggingService is the SPService class which provides an Update and Provision method, we can register the new service farm-wide.

The second step is to create a new Windows Event Log source. Therefore we have to go through the collection of SharePoint farm servers and remotely add the new source by generating registry entries on each server.

To unregister we redo the registration steps by overriding FeatureDeactivating method of the FeatureReceiver class. The UnRegisterLoggingService method then deletes the service and removes all registry keys on all servers in the SharePoint farm.

The sample solution also created an application page to test the logging service. The source code is available as download.

image

Enter a test message and press the Log button. The message will be logged to the Windows Event Log:

And to the SharePoint log files:

image

That’s it.

Download Source Code | Download PDF Version

Using A Custom Authentication Provider For The SharePoint 2010 BCS Administration Object Model

The SharePoint 2010 Business Connectivity Services (BCS) are providing an Administration Object Model to manage all kind of BCS objects. You can use the an Administration Object Model to programmatically create BDC models, LOB system and instances, Entities and Methods. The Microsoft SharePoint Designer is using the object model itself to let you generate External Content Types (ECT).

The sample code is this blog entry is a WPF client application which will read all BDC model names and display them in a ListView control. The application allows the user to enter credentials other than the current Windows user:

The starting point to access the BCS data is the AdministrationMetadataCatalog class. This class is part of the Microsoft.SharePoint.BusinessData.Administration.Client.dll library and namespace. You also need a reference to the Microsoft.BusinessData.dll library.

In order to create an instance of the AdministrationMetadataCatalog class with custom credentials we have to call the GetCatalog method. We need to pass a custom authentication provider as parameter to GetCatalog. A custom authentication provider is a class which implements the ILobiAuthenticationProvider interface. The interface is quite simple, it just asks you to return the user ID (including the domain name) and the according password.

Here the code of your custom authentication provider:

internal class SharePointConnection : ILobiAuthenticationProvider
{
    // . . .

    public SharePointConnection(string userId, string password)
    {
        UserId = userId;
        Password = password;
    }

    public AuthenticationScheme GetAuthenticationScheme(string server, string serverUrl)
    {
        return AuthenticationScheme.RunAs;
    } 

    public string GetCookie(string server, string serverUrl)
    {
        return "BCSCustomAuthenticationProvider";
    }

    public string GetUserId(string server, string serverUrl)
    {
        return UserId;
    }

    public string GetPassword(string server, string serverUrl)
    {
        return Password;
    }
}

Here the sample on how to use the provider:


// Create instance of custom authentication provider 
SharePointConnection spc = new SharePointConnection(UserIdText.Text, PasswordText.Text); 

// Create catalog instance using the custom authentication provider 
AdministrationMetadataCatalog catalog = AdministrationMetadataCatalog.GetCatalog(SiteURLText.Text, spc);

ModelsListView.ItemsSource = catalog.GetModels("*").Select(m => m.Name); 

Important: If you create your own WPF application using the BCS Administration Object Model and reference the Microsoft.SharePoint.BusinessData.Administration.Client.dll library, you need to set the Platform Target to "Any CPU" in the application settings page, otherwise the project will not compile.

Download Source-Code

How To Access SAP Business Data From Silverlight 4 ClientsUsing WCF RIA Services

The introduction of Microsoft’s WCF RIA Services for Silverlight 4 simplified very much the development process of N-tier business applications using Silverlight and ASP.NET. By using this new technology we can also easy access and integrate SAP business data in Silverlight clients.

This article shows how to provide a SAP domain service as web service that will be consumed by a Silverlight client. The sample application will allow the user to query customer data. The service uses LINQ to SAP from Theobald Software to connect to a SAP R/3 system.

Project Setup

The first step in setting up a new Silverlight 4 project with WCF RIA Services is to create a solution using the Visual Studio template Silverlight Navigation Application:



Visual Studio 2010 is then asking you to create an additional web application, which hosts the Silverlight application. It’s important to select the checkbox Enable WCF RIA Services (see screenshot below):


After clicking the Ok button, Visual Studio generates a solution with two projects, one Silverlight 4 project and one ASP.NET project. In the next section we are creating the SAP data access layer using the LINQ to SAP designer.

LINQ to SAP

The LINQ to SAP provider and its Visual Studio 2010 designer offers a very handy way to design SAP interfaces visually. The designer will generate the code for the SAP data access layer automatically, similar to LINQ to SQL. The LINQ provider is part of the .NET library ERPConnect.net from Theobald Software. The company offers a demo version for download on its homepage.

The next step is to create the needed LINQ to SAP file by opening the Add New Item dialog:


LINQ to SAP is internally called LINQ to ERP.

Clicking the Add button will create a new ERP file and opens the LINQ designer. Now, drag the Function object from the toolbox and drop it onto the designer surface. If you have not entered the SAP connection data so far, you are now asked to do so:


Enter the connection data for your SAP R/3 system and then click the Ok button. Next, search for and select the SAP function module named SD_RFC_CUSTOMER_GET. The function module provides a list of customer data.

The RFC Function modules dialog opens and let you define the necessary parameters:


In the above function dialog, change the method name to GetCustomers and mark the Pass checkbox for the NAME1 parameter in the Exports tab. Also set the variable name to namePattern. On the Tables tab mark the Return checkbox for the table parameter CUSTOMER_T and set the table and structure name to CustomerTable and CustomerRow:


After clicking the Ok button and saving the ERP file, the LINQ designer will generate a SAPContext class which contains a method called GetCustomers with an input parameter named namePattern. This method executes a search for SAP customer data allowing the user to enter a wildcard pattern. The method returns a table of customer data:


On the LINQ designer level (click on the free part of the LINQ designer surface) the property Create Object Outside Of Context Class must be set to True:


Now, we finally add a Customer class which we use in our SAP domain service later on. This class and its values will be transmitted to the Silverlight client by the WCF RIA Services. It’s important to set the Key attribute on the identifier fields for WCF RIA Services, otherwise the project will not compile:


That’s it! We now have our SAP data access layer ready to use and can start adding the domain service in the next section.

SAP Domain Service

The next step is to add the SAP domain service to our web project. A domain service is a specialized WCF service and is one of the core constructs of WCF RIA Services. The service exposes operations that can be called from the client generated code. On the client side we use the domain context to access the domain service on the server side.

Add a new Domain Service Class and name it SAPService:


In the upcoming dialog create an empty domain service class by just clicking the Ok button:


Next, we add the service operation GetCustomers to the SAP service with a name pattern parameter. The operation then returns a list of Customer objects. The Query attribute limits the result set to 200 entries.

The operation uses the visually designed SAP data access logic to retrieve the SAP customer data. First of all, an instance of the SAPContext class will be created using a connection string (see sample in code). For more details regarding the SAP connection string see the ERPConnect.net manual.

The LINQ to SAP context class contains the GetCustomers method which we will call using the given namePattern parameter. Next, the operation creates an instance of the Customer class for each customer record returned by SAP.

The license code for the ERPConnect.net library is set in the constructor of our domain service class.


That’s all we need on the server side.

In the next section we are implementing the Silverlight client.

Silverlight Client

The implementation of the client side is straightforward. The home view contains a DataGrid control to display the list of customer data as well as a search area with TextBox and Button controls to allow users to enter name search pattern.

The click event handler of the load button, called OnLoadButtonClick, will execute the SAP service. The boilerplate code to access the web service was generated by WCF RIA Services in the subfolder Generated_Code in the Silverlight project.

First of all, an instance of the SAPContext will be created. Then we load the query GetCustomersQuery and execute the service operation on the server side using WCF RIA Services. If the domain service returns an error, the callback anonymous method will mark the error as handled and display the error message.

If the execution of the service operation succeeded the result set gets displayed in the DataGrid control.


The next screenshot shows the final result:


That’s it.

Summary

This article has shown how easy SAP customer data can be integrate within Silverlight clients using tools like WCF RIA Services and LINQ to SAP. It is quite simple to extend the SAP service to integrate all kind of operations.

Download Source Code

New article about integrating SAP data into SharePoint 2010

I wrote a new article about SharePoint 2010, SAP and BCS (Business Connectivity Services). I have published this article and a demo project with source code on Codeproject:

How To Integrate SAP Business Data Into SharePoint 2010 Using Business Connectivity Services And LINQ to SAP

A PDF version of the article can be downloaded from my Homepage.

Feel free to send me your feedback.

New article about implementing a SharePoint entity repository

I wrote a new article about SharePoint on CodeProject.com (How To Implement A Generic Entity List Repository And Business Logic For SharePoint 2010 Using The T4 Templating Engine).

This article describes how to implement a generic, extensible entity list repository and business logic for SharePoint 2010 using the T4 templating engine (Text Template Transformation Toolkit).

The article is also available as PDF file at the Parago website.

Feel free to send me your feedback.

Version 2.4 of the Validator Toolkit for ASP.NET MVC released

I’ve released a new version (2.4) of the Validator Toolkit for ASP.NET MVC 1.0, that is fixing an issue regarding format changes in the jQuery validation plugin of the minlength, maxlength and rangelength validators.

http://mvcvalidatortoolkit.codeplex.com

Version 2.3 of the Validator Toolkit for ASP.NET MVC released

I’ve released a new version (2.3) of the Validator Toolkit for ASP.NET MVC 1.0, that is fixing an issue regarding format changes in the jQuery validation plugin of the minlength, maxlength and rangelength validators.

http://mvcvalidatortoolkit.codeplex.com