Setting up the Camelot WCF Service for SharePoint

This guide intend to show how to set up the Camelot WCF Service for SharePoint.

Requirements

About Camelot WCF Service and WCF in general

Windows Communication Foundation (WCF) is a framework for building service-oriented applications. Using WCF, you can send data as asynchronous messages from one service endpoint to another. A service endpoint can be part of a continuously available service hosted by IIS, or it can be a service hosted in an application. Any SOAP compatible client can make use of the WCF service, the PHP SoapClient for example, on which the Camelot PHP Tools integration towards the Camelot WCF Service is built.  Further on an endpoint can be a client of a service that requests data from a service endpoint. The messages can be as simple as a single character or word sent as XML, or as complex as a stream of binary data.

The Camelot WCF Service make use of of the Camelot .NET Connector when communicating with SharePoint.

Installation

Begin by downloading the Deployment Package at http://www.bendsoft.com/downloads/camelot-wcf-service/

Add a new web site in the IIS Management console

This step is optional, you can use an existing site as well

 

Deploy the package

  1. Right click on your web site and select Deploy > Import Application
  2. Make sure that all select boxes are checked in the “Select the Contents of the Package” dialog
  3. Enter Application Package Information

It is very important that the Connection String is correct, you can read more about the connection string properties in the Camelot .NET Connector for SharePoint documentation.

Mine is as follows

Server=dev1.spfarm.bendsoft.com;Database=sales;User=sharepointdemo;Password=demopassword;Authentication=Ntlm;TimeOut=60;

The database we select here represents the SharePoint Site you want to connect to. You are able to edit this in the web.config file and add more connection strings.

The web.config

Open the web.config and look for<appSettings>, set the SHARED_KEY value to your preferred value

<appSettings>
    <add key="SHARED_KEY" value="MySharedKey" />
</appSettings>

This is where you will be able to edit your connection strings, or add new ones if needed

<connectionStrings>
    <add name="sharepoint_sales" connectionString="Server=dev1.spfarm.bendsoft.com;
    Database=sales;User=sharepointdemo;Password=demopassword;Authentication=Ntlm;TimeOut=60;" />

    <add name="sharepoint_transport" connectionString="Server=dev1.spfarm.bendsoft.com;
    Database=transport;User=sharepointdemo;Password=demopassword;Authentication=Ntlm;TimeOut=60;" />

    <add name="sharepoint_customers" connectionString="Server=dev1.spfarm.bendsoft.com;
    Database=customers;User=sharepointdemo;Password=demopassword;Authentication=Ntlm;TimeOut=60;" />
</connectionStrings>

Thats it, you are done

As seen on the image there is a new wcf application installed in your web site. To connect to it simply type http://yourserverurl.com/wcf/Camelot.svc and something like this should show up

When clicking the link displayed, or by adding ?wsdl at the end of the url a blank page will be displayed. This is a correct behavior, what you should do is check whats under the surface. Right click and view the source

In the XML displayed above we can actually see the methods enclosed within the wcf service.

Using the WcfTestClient

If you are a developer you might have the WcfTestClient.exe installed, check the folder “C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\”.

The WcfTestClient is a simple suite to check if WCF services really is up and running

 

Remember

The WCF service will install and run without the Camelot.SharepointIntegration library, it’s not until you try to fetch data from SharePoint stuff will break. Follow the installation instructions, if you’re not able to deploy it to the GAC or for some reason dont want to you can simply drop the dll file in the bin folder in your svc service.

 

Get started today. Download the Camelot WCF Service, it’s free!

 

Posted in WCF Services | Leave a comment

Camelot PHP Tools 1.1 for SharePoint released

As of today the next version of the Camelot PHP Tools for SharePoint is available for public download, visit the official web site to download, http://www.bendsoft.com/downloads/sharepoint-php-tools/

Highlights in this update

  • PHP Tools now consorts with Camelot WCF Service
  • Seamless integration with SharePoint (through the WCF Service)

The toolkit has undergone a general stabilization and is more powerful than ever.

Execute Select, Insert, Update and Delete queries in SharePoint lists directly from PHP

The vision for this release was to simplify normally complex integrations to a level where selecting and manipulating data in SharePoint could be done with a single command, from PHP that is. So, here it goes.

Selecting data from SharePoint with SQL

$SharePointQuery = new SharePointQuery(
    array(
        'sql' => "Select * from `My SharePoint List`.`ViewName`",
        'compression' => true,
        'connString' => 'sharepoint_connection',
        'sharedKey' => constant("WSDL_SHARED_KEY")
    )
);

Selecting data from SharePoint by list and view name

$SharePointQuery = new SharePointQuery(
    array(
        'listName' => 'My SharePoint List',
        'viewName' => 'ViewName',
        'includeAttachements' => false,
        'compression' => true,
        'connString' => 'sharepoint_connection',
        'columns' => 'ID;Title;ListItem1;ListItem2',
        'sharedKey' => constant("WSDL_SHARED_KEY")
    )
);

Insert data in SharePoint with SQL and SharePointNonQuery

$SharePointNonQuery = new SharePointNonQuery(array(
    'sql' => "INSERT INTO contactform (title,email,company,message) VALUES ('John Doe','john.doe@example.com','Johns Company','A test message!')",
    'method' => 'ExecuteNonQuery',
    'connString' => 'sharepoint_connection',
    'sharedKey' => constant("WSDL_SHARED_KEY")
));

Delete data in SharePoint with SQL and SharePointNonQuery

$SharePointNonQuery = new SharePointNonQuery(array(
    'sql' => "DELETE FROM contactform WHERE Title = 'John Doe'",
    'method' => 'ExecuteNonQuery',
    'connString' => 'sharepoint_connection',
    'sharedKey' => constant("WSDL_SHARED_KEY")
));

Update data in SharePoint with SQL and SharePointNonQuery

$SharePointNonQuery = new SharePointNonQuery(array(
    'sql' => "UPDATE contactform SET Status = 'Accepted' WHERE `Contact Email` = 'John Doe@example.com'",
    'method' => 'ExecuteNonQuery',
    'connString' => 'sharepoint_connection',
    'sharedKey' => constant("WSDL_SHARED_KEY")
));

Insert data in SharePoint with a Scalar Command

$SharePointNonQuery = new SharePointNonQuery(array(
    'sql' => "INSERT INTO `My List` (title,email) VALUES ('John Doe','john.doe@example.com')",
    'method' => 'ExecuteScalar',
    'connString' => 'sharepoint_connection',
    'sharedKey' => constant("WSDL_SHARED_KEY")
));

// Echo last inserted id
if($SharePointScalarQuery->_result > 0) {
    echo "Last inserted ID is $SharePointScalarQuery->_result";
}

 

$SharePointQuery in a bit more detail

The SharePointQuery is a class who contains methods that initiates a connection to the Camelot WCF Service, it returns a structured Object array (see image below) that is used to display the data.

Decoded Camelot XML Packet

Decoded Camelot XML Packet

Rendering the data

From this point you may choose to do whatever you want with the data, we recommend using the Twig template engine to render the data. The framework is included in PHP Tools and there are some usage examples in the documentation. Below is an example reflecting a list from SharePoint.

try {
    $SharePointQuery = new SharePointQuery(
        array(
            'sql' => "Select ID, LinkTitle as Title, Article, `Publish to external` AS Publish, Created from `SharePoint Articles`.Article",
            'compression' => false,
            'connString' => 'sharepoint_cms',
            'sharedKey' => constant("WSDL_SHARED_KEY")
        )
    );

    $SharePointResult = $SharePointQuery->CamelotSoap->_sorted;

    require_once 'twig/lib/Twig/Autoloader.php';

    Twig_Autoloader::register();
    $loader = new Twig_Loader_Filesystem('templates');
    $twig = new Twig_Environment($loader);
    $template = $twig->loadTemplate('sp2007.html');
    echo $template->render(array(
        'list_header' => $SharePointResult->_schema->display_name,
        'list_content' => $SharePointResult->_content
    ));
} catch (Exception $exc) {
    pr($exc->getTraceAsString());
    pr($exc->getMessage());
}

For the nerds

Lets break that down a bit

$SharePointQuery = new SharePointQuery(

This creates a new instance of the SharePointQuery class which automatically uses the values in the settings.php file (where the WCF service is stored) and open a connection to the WCF service connected to SharePoint.

array(
    'sql' => "Select ID, LinkTitle as Title, Article, `Publish to external` AS Publish, Created from `SharePoint Articles`.Article",
    'compression' => true,
    'connString' => 'sharepoint_cms',
    'sharedKey' => constant("WSDL_SHARED_KEY")
)

This is the command we send to the WCF service, the WCF service executes this towards the SharePoint installation.

  • sql, the command you want to send to SharePoint
  • compression, whether you want to get the returned data compressed or not
  • connString, which connection string to use from the web.config file in the WCF service
  • sharedKey, we implemented some security to disable open access to the methods. This key should correspond to the one set in the web.config file of the WCF service.
$SharePointResult = $SharePointQuery->CamelotSoap->_sorted;

This fetches the returned result and steps in to the object where the data is getting interesting for this purpose (displaying it in a html page).

require_once 'twig/lib/Twig/Autoloader.php';

Twig_Autoloader::register();
$loader = new Twig_Loader_Filesystem('templates');
$twig = new Twig_Environment($loader);
$template = $twig->loadTemplate('sp2007.html');
echo $template->render(array(
    'list_header' => $SharePointResult->_schema->display_name,
    'list_content' => $SharePointResult->_content
));

This loads the twig framework and selects the sp2007 template as output template

  • list_header, the header of the list which comes from the schema
  • list_content, all content rows from the list

Simple as that, this displays the following result

Simple as that, you can use the PHP Tools to form wonderful websites in no time. Download now, it’s free!

Posted in PHP | 2 Comments

SharePoint To Google Calendar Synchronization

SharePoint To Google Calendar Synchronization

The SharePoint calendar can be a useful tool within an organization, allowing team members in different geographical locations to coordinate and share team-related events and meetings. Users would often like to view SharePoint events directly in their personal calendar. SharePoint has a feature that allows you to link your calendars with Outlook, enabling you to look at your calendars side-by-side. In this post I will focus on Google and investigate one-way synchronization from SharePoint to Google using the Camelot .NET Connector and the Google Data API.

Requirements

The examples require that you have a WSS3.0 or MOSS 2007/2010 environment available and Visual Studio 2010 installed on your development computer.

You also need to download and install Camelot .NET Connector from http://www.bendsoft.com together with a valid license key. Developer licenses are free for anyone.

In addition, you need to install the free Google Data API, which allows you to read and update events in your Google calendars.

External links

I found the following links helpful when learning the Google Data API.

Data API Developer’s Guide: .NET

Data API Atom Reference

C# Google Data API Unit Tests (good examples)

Introduction

Single events and recurring events

Calendar events generally fall into two different categories. First we have the single events, which don’t repeat and only appears once in the calendar. The second type is recurring events, which shows up at regular times depending on a given repeating pattern. Either type will have a defined start and end time or it may be a full-day event that lasts for an entire day. While single events are quite simple to handle, recurring events are a little more difficult to work with. The Google API implements the iCalendar (RFC 2445) standard for recurring events. The iCalendar format is widely spread standard for exchanging calendar information between different applications. Unfortunately, SharePoint has its own XML based format for describing recurring events and we need to figure out how to convert between the two.

Selecting events from SharePoint

SharePoint calendar events are stored within lists and can be easily read using the Camelot .NET Connector as shown in the following example. The query used here selects all types of events. The ParticipantsPicker column is synonymous to attendees. The Lookup function expands the user information field and makes it include name and e-mail address.

private static DataTable LoadSharePointEvents(SharePointConnection connection)
{
    using (var adapter = new SharePointDataAdapter("SELECT ID, GUID, Title, Description, Location, EventDate, EndDate, Duration, fAllDayEvent, fRecurrence, RecurrenceData, MasterSeriesItemID, RecurrenceID, EventType, Lookup(ParticipantsPicker) AS Attendees, owshiddenversion FROM `Calendar` ORDER BY ID ASC", connection))
    {
        var dtEvents = new DataTable();
        adapter.Fill(dtEvents);
        return dtEvents;
    }
}

Selecting events from Google

The next example shows how to select events from your default Google calendar. The query is configured to include deleted events and the SingleEvent property tells the server that recurring events should not be expanded, which is typically what you want in synchronization scenarios. The query returns a feed of events known as EventFeed. It is a good thing to specify the max-results option because the default value is set to 25 events.

private static EventFeed LoadGoogleEvents()
{
    var calendarService = new CalendarService("myprogram");
    calendarService.setUserCredentials("myuseraccount.gmail.com", "mypassword");

    var eventQuery = new EventQuery()
    {
        Uri = new Uri("https://www.google.com/calendar/feeds/default/private/full"),
        SingleEvents = false,
        ExtraParameters = "showdeleted=true&max-results=1000"
    };

    var eventFeed = calendarService.Query(eventQuery);
    return eventFeed;
}

Inserting events into Google

The Google calendar API is quite easy to use once you have got into it. Most of the work lies in learning the difference between SharePoint and Google and understanding how to map the information between the two systems. The following example shows how to insert a single (non-repeating) event, represented by the EventEntry class, into Google from one of the data rows selected in the first example.

private static void CreateSingleGoogleEvent(DataRow drEvent)
{
    var eventEntry = new EventEntry();
    string eventUid = ((Guid)drEvent["GUID"]).ToString("N") + "@domain.com";

    eventEntry.Uid = new GCalUid(eventUid);

    eventEntry.Sequence = new GCalSequence()
    {
        IntegerValue = (Int32)drEvent["owshiddenversion"]
    };

    eventEntry.Title.Text = (string)drEvent["Title"];
    eventEntry.Content.Content = (string)drEvent["Description"];

    eventEntry.Locations.Add(new Where()
    {
        ValueString = (string)drEvent["Location"]
    });

    eventEntry.Times.Add(new When()
    {
        StartTime = ((DateTime)drEvent["EventDate"]),
        EndTime = (DateTime)drEvent["EndDate"]
    });

    var calendarService = new CalendarService("myprogram");
    calendarService.setUserCredentials("myuseraccount.gmail.com", "mypassword");
    calendarService.Insert(new Uri("https://www.google.com/calendar/feeds/default/private/full"), eventEntry);
}

Data mapping

The SharePoint calendar list

The calendar list has the following set of columns:

SharePoint column EventEntry property Datatype Description
ID None Int32 Event ID
GUID Uid Guid Globally unique identifier. It maps well to the iCal UID property defined in RFC 2445.
Title Title String Headline
Description Content String Description / Content
Location Locations[] String Location of the event
EventDate Times[].StartTime DateTime Start date and time of the event. For recurring events, the date part may be earlier than then the first instance in the series.
EndDate Times[].EndTime DateTime End date and time of the event. For recurring events, this column normally shows the end time of the last event in the series. For recurring events with no end date, this value is computed several years in the future.
Duration None Int32 Duration (in seconds) of the event
fAllDayEvent Times[].AllDay Boolean Indicates that the event is all-day
fRecurrence None Boolean Indicates that it is a recurring event
RecurrenceData Recurrence String For recurring events, this column contains a recurrence rule in XML format. This content can to be translated into a iCal recurrence rule defined in RFC 2445.
EventType None Int32 [0] Single event
[1] Recurring event
[3] Deleted recurring event
[4] Recurrence exception (modified event)
MasterSeriesItemID None Int32 For deleted or modified recurring events, this is the ID of the master event
RecurrenceID Sequence DateTime For deleted or modified recurring events, this column contains the date and time of the replaced event instance in the series
ParticipantsPicker (Attendees) Participants[] User List of attendees of the event. This column is not visible by default in the Event content type
owshiddenversion Sequence Int32 Event revision number

Converting the recurrence pattern

The recurrence pattern conversion from SharePoint to Google is one of the main tasks. In SharePoint, the recurrence pattern is stored in the RecurrenceData column and typically looks something like:

<recurrence>
  <rule>
    <firstDayOfWeek>su</firstDayOfWeek>
    <repeat>
      <daily weekday="TRUE" />
    </repeat>
    <windowEnd>2011-04-30T07:15:00Z</windowEnd>
  </rule>
</recurrence>

This example describes a daily event, which occurs on each weekday until the April 30th 2011. The time and duration of the events is found in the EventDate and Duration columns.

The corresponding iCal recurrence rule that Google understands is:

RRULE:FREQ=DAILY;WKST=SU;BYDAY=MO,TU,WE,TH,FR;UNTIL=20110430T071500Z;

First day of week

The firstDayOfWeek element specifies the day on which the workweek starts, which is important in some cases. This element corresponds to the iCal WKST rule part and the possible values are SU, MO, TU, WE, TH, FR or SA.

Start date and duration of recurring events

The event start date and time is given by the EventDate column. For recurring events, this value can be set to a date earlier than the first instance. The time part however, gives the exact time for the event and the Duration column shows the event length in seconds. Similarly, the iCal specification has the DTSTART and DURATION/DTEND property pairs.

Generally, date and time can be specified in three ways:

  • Local time without timezone. The time is said “floating” and not bound to a specific time zone. Example: 19701010T120000.
  • Local time with timezone. Example: TZID=Europe/Stockholm:19701010T120000
  • In UTC-time, identified by the “Z” character. Example: 19701010T120000Z

The iCal specification states that the value of DTSTART must be specified with time zone when used with a recurrence rule. It also states that each time zone must have a corresponding VTIMEZONE defintion. However, Google seems to accept known time zones without this definition so we will ignore it for now. In some cases, it can be more convenient to specify DTEND rather than the DURATION.

Example, this event that starts at 9:15 AM Stockholm time and lasts for 30 minutes:

DTSTART;TZID=Europe/Stockholm:20110101T091500

DURATION:PT1800S

Example, an all-day event that starts on December 23 and continues for two days:

DTSTART;VALUE=DATE:20111223;

DTEND;VALUE=DATE:20111225;

End date of recurring events

Recurring events can either end on a specific date, which is given by the windowEnd element or repeat a certain number of times, which is in that case given by the repeatInstances element. If none of these are provided in RecurrenceData, the event has no end date.

For recurring events, the EndDate column normally contains the date and time of the last instance in the series. If the event has no end date, EndDate still contains a computed date several years in the future. It is not very useful for us.

Similary, use the UNTIL rule part to specify end date in the recurrence rule. The specification states that it must be specified in UTC time. Use the COUNT rule part to specify number of occurrences.

Example, daily event that ends after 5 occurences:

RRULE:FREQ=DAILY;COUNT=5;

Example, daily event that ends on June 6th at 15:00 UTC time:

RRULE:FREQ=DAILY;UNTIL=20110606T150000Z;

Daily

Describes an event that occurs:

  • every dayFrequency day(s)
  • every weekday

Example, every second day for all eternity:

<recurrence>
  <rule>
    <firstDayOfWeek>su</firstDayOfWeek>
    <repeat>
      <daily dayFrequency="2" />
    </repeat>
  </rule>
</recurrence>

The dayFrequence attribute corresponds to the INTERVAL rule part and the weekday attribute can be translated to the BYDAY rule part as shown below.

Example, every second day:

RRULE:FREQ=DAILY;INTERVAL=2;

Example, every weekday:

RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR;

Weekly

Describes an event that occurs:

  • every [weekFrequency] week(s) on: [mo,tu,we,th,fr,sa,su]

Example, every third week on wednesdays and fridays:

<recurrence>
  <rule>
    <firstDayOfWeek>su</firstDayOfWeek>
    <repeat>
      <weekly weekFrequency="3" we="true" fr="true"/>
    </repeat>
  </rule>
</recurrence>

The weekFrequence attribute corresponds to the INTERVAL rule part and each day must be parsed into BYDAY as shown below.

Example, every third week on wednesdays and fridays:

RRULE:FREQ=WEEKLY;INTERVAL=3;BYDAY=WE,FR;

Monthly

Describes an event that occurs:

  • every [monthFrequency] month(s) on day [day]

Example, every month on day 25:

<recurrence>
  <rule>
    <firstDayOfWeek>su</firstDayOfWeek>
    <repeat>
      <monthly day="25" />
    </repeat>
  </rule>
</recurrence>

The monthFrequence attribute corresponds to the INTERVAL rule part and the day in month is given by BYMONTHDAY as shown below.

Example, every month on day 25:

RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=25

Yearly

Describes an event that occurs:

  • every [yearFrequency] year(s) on day [day] in month [month]

Example, every year on December 24th:

<recurrence>
  <rule>
    <firstDayOfWeek>su</firstDayOfWeek>
    <repeat>
      <yearly month="12" day="24" />
    </repeat>
  </rule>
</recurrence>

The yearFrequence attribute corresponds to the INTERVAL rule part, the month in the year is given by the BYMONTH rule part and the day in month is given by BYMONTHDAY.

Example, yearly on December 24th:

RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=12;BYMONTHDAY=24;

Monthly by day / Yearly by day

The montly-by-day and yearly-by-day recurrence patterns are extended versions of the two simplier forms, allowing a little more specific scheduling options.

Describes an event that occurs:

  • every [monthFrequency] month(s) on the [weekdayOfMonth=first, second, third, fourth, last] [day, weekday, weekend_day, su-sa]
  • every [yearFrequency] year(s) on the [weekdayOfMonth=first, second, third, fourth, last] [day, weekday, weekend_day, su-sa] in month [month]

Example, every month on the first weekend-day:

<recurrence>
  <rule>
    <firstDayOfWeek>su</firstDayOfWeek>
    <repeat>
      <monthlyByDay weekdayOfMonth="first" weekend_day="true" />
    </repeat>
  </rule>
</recurrence>

Example, every year on the last day of June:

<recurrence>
  <rule>
    <firstDayOfWeek>su</firstDayOfWeek>
    <repeat>
      <yearlyByDay weekdayOfMonth="last" day="true" month="6" />
    </repeat>
  </rule>
</recurrence>

As you can see, the weekdayOfMonth attribute points out a day relative to the beginning or the end of the month. In combination with one of the day, weekday or weekend_day attributes or a named day, it gives you the exact day in the month. These rules can be expressed using a combination of different iCal rule parts.

Example, monthly on the first weekend-day:

RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=SA,SU;BYSETPOS=1;

Example, yearly on the last day of June:

RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=-1;BYMONTH=6;

Recurrence exceptions

Recurrence exceptions are exceptions to the recurrence rules. It can be either deleted or modified instances as indicated by the EventType column. For these events we need to know the MasterSeriesItemID and RecurrenceID values. The MasterSeriesItemID column gives the ID of the master item to which the exception belongs. The RecurrenceID columns provides the date and time of the instance that the exception replaces. Modified events are not specified in the recurrence rule, but instead via the OriginalEvent on the EventEntry. For deleted instances we can simply use the iCal EXDATE property as shown below.

Example, the following event occurs every day at 9:15 AM except on December 24th 2011:

RRULE:FREQ=DAILY;

EXDATE:20111224T091500Z;

Recurrence code implementation

The following piece of code implements the recurrence conversion from SharePoint to Google according to the research in the previous section. The method ConvertRecurrenceRule takes a DataRow with event data and a dictionary containing deleted instances that we know.

private static Recurrence ConvertRecurrenceRule(DataRow drEvent, Dictionary<Int32, List<DateTime>> deletedRecurringInstances)
{
    var doc = new System.Xml.XmlDocument();
    doc.LoadXml((string)drEvent["RecurrenceData"]);

    string iCalRule;

    //start time and duration
    if (!(bool)drEvent["fAllDayEvent"])
    {
        //specify start time and duration for normal events, time-zone is required
        iCalRule = "DTSTART;TZID=Europe/Stockholm:" + ((DateTime)drEvent["EventDate"]).ToString("yyyyMMddTHHmmss") + "rn";
        iCalRule += "DURATION:PT" + ((Int32)(drEvent["Duration"])).ToString() + "Srn";
    }
    else
    {
        //specify start and end date for all day events
        var date = ((DateTime)drEvent["EventDate"]).Date;
        double days = Math.Ceiling((double)(Int32)(drEvent["Duration"]) / 86400);
        iCalRule = "DTSTART;VALUE=DATE:" + date.ToString("yyyyMMdd") + "rn";
        iCalRule += "DTEND;VALUE=DATE:" + date.AddDays(days).ToString("yyyyMMdd") + "rn";
    }

    System.Xml.XmlNode nodeRepeat = doc.SelectSingleNode("recurrence/rule/repeat/*");
    var attributes = new System.Collections.Generic.Dictionary<string, string>();
    foreach (System.Xml.XmlAttribute attribute in nodeRepeat.Attributes)
        attributes.Add(attribute.Name, attribute.Value);

    switch (nodeRepeat.Name)
    {
        case "daily":
            //every [dayFrequency] day(s)
            //every [weekday]
            iCalRule += "RRULE:FREQ=DAILY;";

            if (attributes.ContainsKey("weekday") && attributes["weekday"] == "TRUE")
                iCalRule += "BYDAY=MO,TU,WE,TH,FR;";

            if (attributes.ContainsKey("dayFrequency"))
                iCalRule += "INTERVAL=" + attributes["dayFrequency"] + ";";

            break;
        case "weekly":
            //every [weekFrequency] week(s) on: [mo,tu,we,th,fr,sa,su]
            iCalRule += "RRULE:FREQ=WEEKLY;";

            System.Text.RegularExpressions.MatchCollection byday = System.Text.RegularExpressions.Regex.Matches(nodeRepeat.OuterXml, "(su|mo|tu|we|th|fr|sa)="true"", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
            if (byday.Count > 0)
            {
                iCalRule += "BYDAY=";
                for (int i = 0; i < byday.Count; i++)
                {
                    iCalRule += byday[i].Groups[1].Value.ToUpper();
                    if (i < byday.Count - 1)
                    {
                        iCalRule += ",";
                    }
                }
                iCalRule += ";";
            }

            if (attributes.ContainsKey("weekFrequency"))
                iCalRule += "INTERVAL=" + attributes["weekFrequency"] + ";";

            break;
        case "monthly":
            //every [monthFrequency] month(s) on day [day]
            iCalRule += "RRULE:FREQ=MONTHLY;";

            if (attributes.ContainsKey("day"))
                iCalRule += "BYMONTHDAY=" + attributes["day"] + ";";

            if (attributes.ContainsKey("monthFrequency"))
                iCalRule += "INTERVAL=" + attributes["monthFrequency"] + ";";

            break;
        case "monthlyByDay":
        case "yearlyByDay":
            //every [monthFrequency] month(s) on the [weekdayOfMonth=first,second,third,fourth,last] [day,weekday,weekend_day,su-sa]
            //every [yearFrequency] year(s) on the [weekdayOfMonth=first,second,third,fourth,last] [day,weekday,weekend_day,su-sa] in month [month]
            iCalRule += "RRULE:FREQ=" + (nodeRepeat.Name == "monthlyByDay" ? "MONTHLY" : "YEARLY") + ";";

            string weekdayOfMonth = "";
            if (attributes.ContainsKey("weekdayOfMonth"))
            {
                switch (attributes["weekdayOfMonth"])
                {
                    case "first":
                        weekdayOfMonth = "1";
                        break;
                    case "second":
                        weekdayOfMonth = "2";
                        break;
                    case "third":
                        weekdayOfMonth = "3";
                        break;
                    case "fourth":
                        weekdayOfMonth = "4";
                        break;
                    case "last":
                        weekdayOfMonth = "-1";
                        break;
                }
            }

            if (attributes.ContainsKey("day") && attributes["day"] == "TRUE")
                iCalRule += "BYMONTHDAY=" + weekdayOfMonth + ";";

            if (attributes.ContainsKey("weekday") && attributes["weekday"] == "TRUE")
            {
                iCalRule += "BYDAY=MO,TU,WE,TH,FR;";
                iCalRule += "BYSETPOS=" + weekdayOfMonth + ";";
            }

            if (attributes.ContainsKey("weekend_day") && attributes["weekend_day"] == "TRUE")
            {
                iCalRule += "BYDAY=SA,SU;";
                iCalRule += "BYSETPOS=" + weekdayOfMonth + ";";
            }

            System.Text.RegularExpressions.Match namedday = System.Text.RegularExpressions.Regex.Match(nodeRepeat.OuterXml, "(su|mo|tu|we|th|fr|sa)="true"", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
            if (namedday.Success)
                iCalRule += "BYDAY=" + weekdayOfMonth + namedday.Groups[1].Value.ToUpper() + ";";

            if (attributes.ContainsKey("month"))
                iCalRule += "BYMONTH=" + attributes["month"] + ";";

            if (attributes.ContainsKey("monthFrequency"))
                iCalRule += "INTERVAL=" + attributes["monthFrequency"] + ";";

            if (attributes.ContainsKey("yearFrequency"))
                iCalRule += "INTERVAL=" + attributes["yearFrequency"] + ";";

            break;
        case "yearly":
            //every [yearFrequency] year(s) on day [day] in month [month]
            iCalRule += "RRULE:FREQ=YEARLY;";

            if (attributes.ContainsKey("day"))
                iCalRule += "BYMONTHDAY=" + attributes["day"] + ";";

            if (attributes.ContainsKey("month"))
                iCalRule += "BYMONTH=" + attributes["month"] + ";";

            if (attributes.ContainsKey("yearFrequency"))
                iCalRule += "INTERVAL=" + attributes["yearFrequency"] + ";";

            break;
    }

    //first day of the week
    System.Xml.XmlNode nodeFirstDayOfWeek = doc.SelectSingleNode("recurrence/rule/firstDayOfWeek");
    if (nodeFirstDayOfWeek != null)
        iCalRule += "WKST=" + nodeFirstDayOfWeek.InnerText.ToUpper() + ";";

    //recurrence repeat count
    System.Xml.XmlNode nodeRepeatInstances = doc.SelectSingleNode("recurrence/rule/repeatInstances");
    if (nodeRepeatInstances != null)
    {
        var repeatInstances = Int32.Parse(nodeRepeatInstances.InnerText);
        iCalRule += "COUNT=" + repeatInstances.ToString() + "rn";
    }

    //recurrence end date (must be in universal time)
    System.Xml.XmlNode nodeWindowEnd = doc.SelectSingleNode("recurrence/rule/windowEnd");
    if (nodeWindowEnd != null)
    {
        var windowEnd = DateTime.ParseExact(nodeWindowEnd.InnerText, "yyyy-MM-ddTHH:mm:ssZ", null);
        iCalRule += "UNTIL=" + windowEnd.ToUniversalTime().ToString("yyyyMMddTHHmmssZ") + "rn";
    }

    if (nodeRepeatInstances == null && nodeWindowEnd == null)
        iCalRule += "rn";

    //deleted instances of the recurring event
    if (deletedRecurringInstances.ContainsKey((Int32)drEvent["ID"]))
    {
        foreach (DateTime deletedTime in deletedRecurringInstances[(Int32)drEvent["ID"]])
            iCalRule += "EXDATE:" + deletedTime.ToUniversalTime().ToString("yyyyMMddTHHmmssZ") + "rn";
    }

    return new Recurrence() { Value = iCalRule };
}

Google calendar synchronization FAQ

In this post I have outlined the basics for how to use the Camelot .NET Connector and the Google Data API to build rich calendar synchronization tools. We have also seen how to convert recurrence rules, which is a big part of the work. The next step is to design a complete and comprehensive solution for the whole synchronization process.

Bendsoft is planning to release services and open source tools for various synchronization tasks in short time including calendar synchronization. We will present more details on this site as soon as possible!

The following section contains answers to some frequently asked questions related to the Google Data API or calendar synchronization in general. It felt like a good idea to share these answers on one place!

My friends are being spammed! How do I prevent Google from sending notifications and invitations to attendees when inserting or updating events?

Use SyncEvent to prevent all UI-related activities, such as inviting attendees with email. Setting this property to true tells the server that the operation is going to be in “sync” mode. SyncEvent cannot be used with non-primary calendars without setting the “X-Redirect-Calendar-Shard: true” header.

Why does Google give error ”syncEvent requires the X-Redirect-Calendar-Shard header”?

This error occurs when you try set SyncEvent on a non-primary calendar. For some reason, you need to set the ”Redirect-Calendar-Shard: true” header to be able to do so. As far as I can see, there is no reason why you cannot always set this.

((GDataRequestFactory)calendarService.RequestFactory).CustomHeaders.Add("X-Redirect-Calendar-Shard: true");

Why can’t I get more than 25 events from Google?

By default, maximum 25 events are returned from the server. You can change this through the max-results query parameter.

How can I detect deleted Google events?

When an event is deleted, it is assigned status CANCELED and no longer shows up in the UI. You can tell the server to include deleted events by adding the showdeleted query parameter.

How can I restore a deleted Google event?

You can retrieve the deleted event and change the event status from CANCELED to CONFIRMED.

I am trying to restore a deleted Google event but it does not work. What am I doing wrong?

Make sure that you have set the SyncEvent property. That should do it!

How do I stop recurring events from being expanded?

Set the SingleEvents property to false on the EventQuery to NOT expand recurring events.

How do I create recurrence exceptions in Google?

Create a new event with the same UID as the master event and configure the OriginalEvent property. The master event must be created before creating the recurrence exception.

eventEntry.Uid = masterEventEntry.Uid;
eventEntry.OriginalEvent = new OriginalEvent()
{
    IdOriginal = masterEventEntry.EventId,
    OriginalStartTime = new When() { StartTime = (DateTime)drEvent["RecurrenceID"] },
    Href = masterEventEntry.SelfUri.ToString()
};

Why do I get error “Participant is neither attendee nor organizer”?

This happens when you try to insert events into a non-primary calendar. You need to add the calendar ID (randomcharacters@group.calendar.google.com) as organizer to participants.

How do I find the ID of a non-primary calendar?

In Google, open calendar settings for the calendar and scroll down to the “Calendar Address” section where you will find the calendar ID printed.

I am trying to synchronize SharePoint “Attendees” . How can I see the user’s e-mail?

You must set the ExpandUserFields option to true in your connection string. This will expand the information displayed in user columns.

How do I choose UID when synchronzing SharePoint events?

Since every item in SharePoint already has its own GUID it makes sense to use this. It allows tracking every event to its original source. The specification recommends that a domain suffix is added on the right side of the UID separated by the character @.

Example UID: 21EC20203AEA1069A2DD08002B30309D@mysharepointdomain.com

Can I search for event UID in my Google calendar?

No, it is not possible. You can however add an extended property to all your events. Extended properties can be searched for using the extq query parameter.

Example: extq=[uid:21EC20203AEA1069A2DD08002B30309D@mysharepointdomain.com]

What is sequence number?

The sequence number is the event revision number as defined in RFC 2445. It should be incremented by the organizer whenever significant changes are done. This can be mapped to the SharePoint item version (owshiddenversion).

Posted in Google, SharePoint | 2 Comments

Using SharePoint as a CMS backend/data layer

Almost all data in SharePoint are stored in lists, each list has it’s own columns and each column got it’s own content. So far everything seems as any normal database, but in SharePoint everything has been lifted both one and two levels higher than a normal database. Some of the biggies is that you can define your own content types and define your own views of these lists.

Advantages when using SharePoint as a CMS backend/data layer

Any developer knows the fact that there’s almost always time and money for forward development (i.e new functionality and front end) but never enough time or funding to develop the backside in a proper manner. When using SharePoint as a backend/data layer developers can focus on the business logic and front end since the backend with all necessary tools already is there.

  1. Versatile database: The foundation of SharePoint is made to store and organize both small and large amounts of various data. The data can vary from anything between simple text, documents, pictures and so on, just about anything can be stored in SharePoint
  2. Stay organized: All data stored in these SharePoint lists can be organized with filters in different views which enables each user or role to have relevant views of the data
  3. Transparency: All data becomes transparent; usually only developers with a SQL-gui tool can see all the data stored in the data layer. With SharePoint everyone with access to the parts storing list/table data can view and edit whats there
  4. Authentication: SharePoint has a built-in account management and if you prefer you can always use an external authentication provider or combine the built-in accounts with external accounts to manage all access to the SP installation
  5. Security: The data is organized in sites, below the sites you have lists who contain all content (content rows actually). You can set access and handling rights at any level, usually on the actual site, the lists or even on a specific row in a specific list
  6. Built in workflows: SharePoint enables you to attach custom business processes to documents or list items called workflows. A workflow is a natural way to organize and run a set of work units, or activities, to form an executable file representation of a work process. This process can control almost any aspect of an item in SharePoint, including the life cycle of that item. The workflow is flexible enough to model both the system functions and human actions necessary for the workflow to complete
  7. Simplicity: Any action above can be maintained by pretty much anyone, even “office dummies”. The interfaces and workspace is very intuitive and simple to use, any office user will instantly feel at home.

Summed together these factors make SharePoint to the best CMS backend ever built, the problem is the last mile input and output of data. The solution is the Camelot ADO.NET connector for SharePoint which converts SharePoint to a SQL Data Layer!


You can read more and download the
Camleot .NET Connector for SharePoint here


Posted in SharePoint | Leave a comment

Display your SharePoint blog posts in a SP Web Part

The built-in blog tool in SharePoint 2010 and SharePoint 2007 is a basic blog tool with everything you need to quickly share ideas and information in your organization. The blog template exists in all SharePoint versions since 2007 which includes all server versions as well as the free wss and Foundation distributions.

A quick research shows that a common request is to show the SharePoint blog posts on multiple SharePoint sites and possibly on external web sites, but lets talk about that in another blog post.

The bad news is that this type of internal integrations quickly becomes very complex and proprietary using CAML, XSLT and the built-in SharePoint Class Libraries and WS References (have a look)

The good news is that when using the Camelot .NET Connector for SharePoint we can build a very versatile and modular web part with just a few functions.

SharePoint blog site basics

The SharePoint blog is a site template that contains lists and libraries, such as a list of blog posts, a list of other blogs (SharePoint 2007 only), and a library for photos. Once you create a blog, you can set up categories, and then customize the blog settings.

Available lists

  • Categories
  • Comments
  • Links
  • Other Blogs (SharePoint 2007 only)
  • Photos
  • Posts

The design

Instead of pushing the posts to lists all over the sites we will build a web part who is able to fetch blog posts filtered by their category. One simple but important design difference between the SharePoint 2007 and 2010 versions is that in the 2010 versions you are able to select multiple categories to a blog post.

Wish list

The design specification tells me that it should be a simple and modular web part who can

  • Connect to any SharePoint installation (which we can reach) and fetch blog posts
  • Support both SharePoint 2007 and 2010
  • Fetch blog posts from a single or multiple categories, no category selected should fetch all posts
  • Total posts to fetch
  • Decide sort order
  • Display blog titles only, the titles must have a href link to the actual post
  • Specify how many words to display from retrieved posts
  • Display a read more link in the end of each excerpt.

Thats not much is it?

You can read more and download the source and
binaries for this web part here

The code

Requirements

/***************************************
 * Camelot "Camelot SP Blog Reader" for SharePoint
 * @version 1.0
 * @author Bendsoft
 * @package Camelot SP Blog Reader 
 * @subpackage Camelot
 * @license FreeBSD License

 * Bendsoft 2011 
 * www.bendsoft.com
***************************************/


using System;
using System.ComponentModel;
using System.Data;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;
using Camelot.SharePointConnector.Data;

namespace CamelotSPBlogReader
{

    public class CamelotSPBlogReader : System.Web.UI.WebControls.WebParts.WebPart
    {
        //Microsoft.SharePoint.WebPartPages.WebPartPage
        // Set up connection to SharePoint
        private string connectionString = string.Empty;
        private SharePointConnection conn;
        private System.Version v = Microsoft.SharePoint.Administration.SPFarm.Local.BuildVersion;

        #region ConnectionString

        private string serverAddress;
        [Category("ConnectionString"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("Server address"),
        WebDescription("The IP or hostname of the SharePoint server. Required.")]
        public string ServerAddress
        {
            get
            {
                if (serverAddress != null)
                    return serverAddress;
                else
                    return "localhost";
            }
            set { serverAddress = value; }
        }

        private string userName;
        [Category("ConnectionString"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("User name"),
        WebDescription("User name of the account used to access SharePoint. This account must have sufficient privileges to access the provided site. Required when server uses authentication mode [Ntlm] or [Basic].")]
        public string UserName
        {
            get
            {
                if (userName != null)
                    return userName;
                else
                    return string.Empty;
            }
            set { userName = value; }
        }

        private string userPassword;
        [Category("ConnectionString"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("Password"),
        WebDescription("Password for the actual user")]
        public string UserPassword
        {
            get
            {
                if (userPassword != null)
                    return userPassword;
                else
                    return string.Empty;
            }
            set { userPassword = value; }
        }

        private string userDomain;
        [Category("ConnectionString"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("User Domain (optional)"),
        WebDescription("The domain of the specified user. Leave empty if domain user account is not used.")]
        public string UserDomain
        {
            get
            {
                if (userDomain != null)
                    return userDomain;
                else
                    return string.Empty;
            }
            set { userDomain = value; }
        }

        private string siteName;
        [Category("ConnectionString"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("Site Name"),
        WebDescription("Corresponds to the site path on the SharePoint server, e.g. marketing or marketing/news. Leave empty for top site.")]
        public string SiteName
        {
            get { return siteName; }
            set { siteName = value; }
        }

        private string authentication;
        [Category("ConnectionString"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("Authentication"),
        WebDescription("Indicates the authentication mode used on the SharePoint server. [Default]: Use the credentials from the current security context. No specific account details should be provided. Only set this mode when SPC is used in components running inside SharePoint, such as webparts. SPC is then running as the current user. [Ntml]: Specifies client authentication using Ntml. This is the most common scenario. [Basic]: Specifies that user authenticates to the server using basic authentication which is a simpler less secured form.")]
        public string Authentication
        {
            get
            {
                if (authentication != null)
                    return authentication;
                else
                    return "Default";
            }
            set { authentication = value; }
        }

        #endregion

        #region Property Blog feed settings

        public enum SelectVersionEnum { Version2010, Version2007 };
        protected SelectVersionEnum selectVersion;
        [Category("SharePoint Blog Settings"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("SharePoint Version"),
        WebDescription("SharePoint version where the blog is kept. Version2010 means SharePoint 2010 or newer, Version2007 means SharePoint 2007 or older.")]
        public SelectVersionEnum SelectVersion
        {
            get { return selectVersion; }
            set { selectVersion = value; }
        }

        private string blogCategory;
        [Category("SharePoint Blog Settings"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("Category/Categories"),
        WebDescription("Type category/categories to fetch, separate with semicolon.")]
        public string BlogCategory
        {
            get { return blogCategory; }
            set { blogCategory = value; }
        }

        private int posts;
        [Category("SharePoint Blog Settings"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("No. posts"),
        WebDescription("How many posts to fetch")]
        public int Posts
        {
            get
            {
                if (posts != 0)
                    return posts;
                else
                    return 5; // Let's have 5 as default!
            }
            set { posts = value; }
        }

        public enum TitlesByEnum { FALSE, TRUE }; // Set default value first!
        protected TitlesByEnum titlesOnly;
        [Category("SharePoint Blog Settings"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("Show titles only"),
        WebDescription("If TRUE the control only renders the title link.")]
        public TitlesByEnum TitlesOnly
        {
            get { return titlesOnly; }
            set { titlesOnly = value; }
        }

        public enum SortByEnum { DESC, ASC }; // Set default value first!
        protected SortByEnum sortOrder;
        [Category("SharePoint Blog Settings"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("Sort order"),
        WebDescription("ASC = Oldest post first, DESC = Newest post first")]
        public SortByEnum SortOrder
        {
            get { return sortOrder; }
            set { sortOrder = value; }
        }

        private int wordCount;
        [Category("SharePoint Blog Settings"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("No. words"),
        WebDescription("How many words to display, 0 will render the entire post.")]
        public int WordCount
        {
            get
            {
                return wordCount;
            }
            set { wordCount = value; }
        }

        public enum ShowByEnum { TRUE, FALSE }; // Set default value first!
        protected ShowByEnum showReadMore;
        [Category("SharePoint Blog Settings"),
        WebBrowsable(true),
        Personalizable(PersonalizationScope.Shared),
        WebDisplayName("Show read more link"),
        WebDescription("If TRUE the control renders a read more link.")]
        public ShowByEnum ShowReadMore
        {
            get { return showReadMore; }
            set { showReadMore = value; }
        }

        #endregion

        /// <summary>
        /// Base method to construct the web part
        /// </summary>
        /// <returns></returns>
        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            try
            {
                /** Set up connection to your SharePoint server **/
                connectionString = "Server=" + ServerAddress + ";Database=" + SiteName + ";Domain=" + UserDomain + ";User=" + UserName + ";Password=" + UserPassword + ";Authentication=" + Authentication + ";TimeOut=60;decodename=true";
                conn = new SharePointConnection(connectionString);

                /** Use the buildCategoryQuery to build the conditional query for selected categories **/
                string sqlCategories = buildCategoryQuery();

                /** Build query for Posts list **/
                string sqlLimit = (posts > 0) ? string.Format(" LIMIT {0}", posts) : "";
                string sqlSort = string.Format(" ORDER BY PublishedDate {0}", sortOrder);

                string sql = (titlesOnly.ToString() == "TRUE") ?
                    string.Format("SELECT Title, ID FROM `Posts` {0} {1} {2}", sqlCategories, sqlSort, sqlLimit) :
                    string.Format("SELECT Title, Body, ID FROM `Posts` {0} {1} {2}", sqlCategories, sqlSort, sqlLimit);
                SharePointDataAdapter adapter = new SharePointDataAdapter(sql, conn);

                DataTable dt = new DataTable();
                adapter.Fill(dt);

                /** Output **/
                foreach (DataRow row in dt.Rows)
                {
                    if (titlesOnly.ToString() == "TRUE")
                    {
                        // Header
                        Controls.Add(new LiteralControl(string.Format("<h2 class=""><a href="/{0}/Lists/Posts/Post.aspx?ID={1}">{2}</a></h2>", SiteName, row["ID"], row["Title"])));
                    }
                    else
                    {
                        // Header
                        Controls.Add(new LiteralControl(string.Format("<h2 class=""><a href="/{0}/Lists/Posts/Post.aspx?ID={1}">{2}</a></h2>", SiteName, row["ID"], row["Title"])));

                        // Body
                        Controls.Add(new LiteralControl(string.Format("<p class="">{0}</p>", truncateAtWord(row["Body"].ToString()))));

                        // Read More link
                        if (showReadMore.ToString() == "TRUE")
                            Controls.Add(new LiteralControl(string.Format("<a class="" style="text-align: right;" href="/{0}/Lists/Posts/Post.aspx?ID={1}">Read more</a>", SiteName, row["ID"])));
                    }
                }
            }
            catch (Exception a) { Controls.Add(new LiteralControl(a.Message)); }

        }

        /// <summary>
        /// Builds a partial SQL string to match values in the Post list.
        /// </summary>
        /// <returns></returns>
        private string buildCategoryQuery()
        {

            string sqlCategories = string.Empty;
            string sqlWhere = string.Empty;
            string sqlReturn = string.Empty;

            if (!string.IsNullOrEmpty(blogCategory))
            {

                // Handle the input
                string[] blogCategories = blogCategory.Split(new char[] { ',' });

                switch (selectVersion.ToString())
                {
                    case "Version2007":
                        // Get category id's. We should be able to query the Posts table directly (looking for CategoryWithLink) but 
                        // some SharePoint setups wont allow us to query computed values through the api, thus this small detour.
                        // This mainly applies to SharePoint 2007 or older releases

                        // Build conditions for the query in the categories table
                        for (int i = 0; i < blogCategories.Length; i++)
                        {
                            if (i < blogCategories.Length - 1)
                                // If several
                                sqlWhere += string.Format(" LinkTitle = '{0}' OR ", blogCategories[i].Trim());
                            else
                                // If single or last
                                sqlWhere += string.Format(" LinkTitle = '{0}' ", blogCategories[i].Trim());
                        }

                        // Fetch category ID's
                        sqlCategories = string.Format("select ID from categories where {0}", sqlWhere);
                        SharePointDataAdapter adapter = new SharePointDataAdapter(sqlCategories, conn);
                        DataTable dt = new DataTable();
                        adapter.Fill(dt);

                        // Build querystring
                        if (dt.Rows.Count > 0)
                        {
                            sqlReturn = " WHERE ";
                            int i = 0;

                            foreach (DataRow row in dt.Rows)
                            {
                                i++;
                                sqlReturn += string.Format(" PostCategory = {0} ", row["ID"]);

                                // Add OR if not last row
                                if (i < dt.Rows.Count)
                                    sqlReturn += " OR ";
                            }
                        }
                        break;

                    case "Version2010":
                    default:
                        // In SharePoint 2010 or newer we can query the Posts list directly with a string value 
                        // matching the values in CategoryWithLink

                        if (blogCategories.Length > 0)
                        {
                            for (int i = 0; i < blogCategories.Length; i++)
                            {
                                if (i < blogCategories.Length - 1)
                                    // If several
                                    sqlWhere += string.Format(" CategoryWithLink = '{0}' OR ", blogCategories[i].Trim());
                                else
                                    // If single or last
                                    sqlWhere += string.Format(" CategoryWithLink = '{0}' ", blogCategories[i].Trim());
                            }

                            sqlReturn = " WHERE " + sqlWhere;
                        }
                        break;

                }
            }
            return sqlReturn;

        }

        /// <summary>
        /// Cut the input string after n words
        /// </summary>
        /// <param name="input"></param>
        /// <returns>Ready-formated string</returns>
        private string truncateAtWord(string input)
        {
            try
            {
                string output = string.Empty;
                string[] inputArr = input.Split(new char[] { ' ' });

                if (inputArr.Length <= wordCount)
                    return input;

                if (wordCount > 0)
                {
                    for (int i = 0; i < wordCount; i++)
                    {
                        output += inputArr[i] + " ";
                    }
                    output += "...";
                    return output;
                }
                else
                {
                    return input;
                }
            }
            catch (Exception a)
            {
                return a.Message;
            }

        }

        /// <summary>
        /// Find and return a hint of the current SharePoint version
        /// </summary>
        /// <returns></returns>
        private string VersionInformation()
        {
            string versionReturn = string.Empty;
            if (Convert.ToInt32(v.Major) <= 12)
                versionReturn = "2007 or older.";
            else
                versionReturn = "2010 or newer.";

            return string.Format("Hint! You are using SharePoint {0}", versionReturn);
        }

    }
}

The settings dialogs will look like this

You can download the entire Visual Studio project or precompiled binary at our official web site www.bendsoft.com, the project is stored at the following url http://www.bendsoft.com/downloads/sharepoint-web-parts/sharepoint-blog-reader/

Enjoy

Posted in Integrations, SharePoint, SharePoint Web Part Development | 1 Comment

Creating a simple “Contact Us” form

SharePoint is a powerful application for managing and sharing information and documents within companies. Nevertheless, many organisations are still using other platforms or content management systems side-by-side. This does not prevent you from using SharePoint as a central sharing point for any of these. Camelot .NET Connector enables quick integration with other systems, for example Joomla or Umbraco.

This article will show, step-by-step, how to create a simple contact form in ASP.NET. Contact forms are typically used on a public website to provide a way for customers, business partners and others to submit questions or request information. This is a great example of using SharePoint in a creative way to simplify common tasks in any organisation. This example can practically be implemented as is in Umbraco.

Requirements

This example requires that you have a WSS3.0 or MOSS 2007/2010 environment available and Visual Studio 2010 installed on your development computer. You also need to download and install Camelot .NET Connector from http://www.bendsoft.com together with a valid license key. Developer licenses are free for anyone. I assume that you already know how to create and edit lists in SharePoint and familiar with the Visual Studio 2010 IDE.

Preparing SharePoint

The first thing to do is to create a custom SharePoint list in WSS 3.0 or MOSS 2007/2010 for storing incoming contact requests. In this example, we choose to call our new list ContactForm. We will also create a set of new columns to hold the details.

The contact list is defined by the columns Title, Company, Email and Message where Title will be used to store the name of the contact person. Message is created as a multi-line text column and the other holds a single line of text. This is what the empty list default view will looks like.

Creating a user control

Now we will create an ASP.NET web user control, which can then be placed on any ASP.NET web page within out site. In Visual Studio 2010, add a new Web User Control (language C#) to your web site and call it contactcontrol.ascx. Ensure that “Place code in separate file” is checked.

contactcontrol.ascx

Replace the content of the markup file (contactcontrol.ascx) with the following lines. We have added four input fields for name, e-mail address, company and message. The message input allows multiple lines. We have also added required field validation to the name, e-mail and message, leaving company optional. Besides, the e-mail address must be in valid format. Finally, I have added a submit button in the bottom of the form and hooked up the Submit_Click method on the OnClick event.

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="contactcontrol.ascx.cs" Inherits="contactcontrol" %>
<asp:Panel ID="controlpanel" runat="server">
    <h2>Contact Us</h2>
    <p>Please use the form below to contact us. We will get back to you as quick as possible!</p>
    <asp:ValidationSummary ID="errors" runat="server" DisplayMode="BulletList" />
    <table>
        <tr>
            <td>Name:</td>
            <td><asp:TextBox ID="name" runat="server"></asp:TextBox> *</td>
        </tr>
        <tr>
            <td>E-mail address:</td>
            <td><asp:TextBox ID="email" runat="server"></asp:TextBox> *</td>
        </tr>
        <tr>
            <td>Company:</td>
            <td><asp:TextBox ID="company" runat="server"></asp:TextBox></td>
        </tr>
        <tr>
            <td>Message:</td>
            <td><asp:TextBox ID="message" runat="server" TextMode="MultiLine" Rows="10"></asp:TextBox> *</td>
        </tr>
    </table>
    <asp:Button ID="Submit" runat="server" Text="Submit Request" OnClick="Submit_Click" />
    <asp:RequiredFieldValidator ID="name_validator" runat="server" ControlToValidate="name" ErrorMessage="Name is missing" Display="None"></asp:RequiredFieldValidator>
    <asp:RequiredFieldValidator ID="email_validator_1" runat="server" ControlToValidate="email" ErrorMessage="E-mail is missing" Display="None"></asp:RequiredFieldValidator>
    <asp:RegularExpressionValidator ID="email_validator_2" runat="server" ControlToValidate="email" ErrorMessage="Invalid E-mail address" ValidationExpression="^([w-.]+)@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.)|(([w-]+.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(]?)$" Display="None"></asp:RegularExpressionValidator>
    <asp:RequiredFieldValidator ID="message_validator" runat="server" ControlToValidate="message" ErrorMessage="Message cannot be left empty" Display="None"></asp:RequiredFieldValidator>
</asp:Panel>

contactcontrol.ascx.cs

Open up the associated code file (contactcontrol.ascx.cs) and replace the content with the following code. The CssClass property is a quick way to enable setting the css class of the control. The most important section is the Submit_Click method which handles our submit button’s click event. The method first checks that the page is valid and then prepares and executes an insert query into our SharePoint list. The connection string is stored in the web.config ad named sharepoint_sales. Name this whatever you like and add the corresponding option to your web.config. The following connection string template can be used in most cases. More connection string options are descibed in the connector documentation found on the bendsoft website.

<add name="sharepoint_sales" connectionString="Server=my.sharepointserver.com;Database=customers;User=myuser;Password=mypass;Authentication=Ntlm;TimeOut=60;" />

Using parameters ensures that the data is inserted safely without need for escaping special characters. After inserting the post, the page shows a confirmation message. Note: Exception handling needs to be added before used in production.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Camelot.SharePointConnector.Data;

public partial class contactcontrol : System.Web.UI.UserControl
{
    public string CssClass {get; set;}

    protected void Page_Load(object sender, EventArgs e)
    {
        controlpanel.CssClass = this.CssClass;
    }


    protected void Submit_Click(object sender, EventArgs e)
    {
        if (Page.IsValid)
        {
            using (SharePointConnection connection = new SharePointConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings["sharepoint_sales"].ConnectionString)) 
            {
                connection.Open();
                using (SharePointCommand command = new SharePointCommand("INSERT INTO `ContactForm` SET Title = @name, `Email` = @email, `Company` = @company, Message = @message", connection))
                {
                    command.Parameters.Add("@name", this.name.Text);
                    command.Parameters.Add("@email", this.email.Text);
                    command.Parameters.Add("@company", this.company.Text);
                    command.Parameters.Add("@message", this.message.Text);
                    command.ExecuteNonQuery();

                    this.message.Text = "";
                    this.Page.ClientScript.RegisterStartupScript(this.GetType(), "Success", "alert('Your question has been sent to our team. We will handle your enquiry as soon as possible');", true);
                }
            }
        }
    }
}

Creating a sample page

Add a new web page to your project called contactus.aspx and replaced the content with the following markup code. On the top of the page, we register our new contact control and later down we add an instance of this control to the page. We have also added some css styling to the page to improve the visual impression.

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="contactus.aspx.cs" Inherits="contactus" %>
<%@ Register TagPrefix="cc" TagName="ContactControl" Src="contactcontrol.ascx" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Contact Us</title>
    <style type="text/css">
        body 
        {
            font-family:Trebuchet MS;
            font-size:12px;
        }
        .contactcontrol td
        {
            vertical-align:top;
        }
        .contactcontrol input
        {
            font-family:Trebuchet MS;
            font-size:12px;
        }
        .contactcontrol input[type="text"]
        {
            width:200px;
            border:solid #cccccc 1px;
        }
        .contactcontrol textarea
        {
            width:300px;
            border:solid #cccccc 1px;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <div>
         <cc:ContactControl ID="contactcontrol" runat="server" CssClass="contactcontrol" />   
    </div>
    </form>
</body>
</html>

Testing our page

Now, start your web site and browse to the contactus.aspx page. If everything goes well, you should be seeing the contract form. Don’t forget to add the connection string to your web.config first!

Test your form by adding a new post. You will get a confirmation from the web site that the post has been added.

Return to your SharePoint environment where your should be seeing the new entry in your ContactForm list.

Final word

The Camelot .NET Connector simplifies SharePoint integration. In fact, in many cases, .NET developers do not need to have any previous experience of SharePoint development. By using your general .NET skills and basic understanding of the SQL syntax and other ADO.NET drivers, you will be able to achive quite alot on your own. This is why the connector gives you advantage over you competitors.

If you any questions regarding the connector and how it can help you, please feel free to contact us using our very own contact form, of course powered by SharePoint 2010, at http://www.bendsoft.com/contact-us/.

Posted in SharePoint | Leave a comment

New website

Don’t miss our brand new commercial portal, www.bendsoft.com.

We intend to pack it with SharePoint Web Parts, tutorials, useful SharePoint integrations and other cool stuff.

Leave a comment

Selecting with Camelot .NET Connector

In this post I will cover the basics of the SELECT syntax in the Camelot .NET Connector. I hope you will find this information useful when getting started with the connector. Feel free to ask any questions if something is missing or unclear.

The syntax offers the ability to select all or a subset of columns from a specified SharePoint list, optionally providing  a view that belongs to that list. The WHERE statement is used to filter the results based on conditions of your choice. Finally, the results can be ordered by one or more columns using the ORDER BY statement and limited to specified number of rows using the LIMIT statement.

The full syntax is described:

SELECT 
	{col_name [AS alias_name] [, col_name ...] | *} 
	FROM list_name 
		[.{view_name | content_type_name | ALL | ATTACHMENTS}] 
	[WHERE where_condition] 
	[ORDER BY col_name 
	[ASC | DESC] [, col_name ...]] 
	[LIMIT row_count]

Suppose that our team has a common calendar list called “Team calendar“. We have added a few entries for February and March as shown below in the “All Events” view.

Select .. From

To select all columns from the default calendar view, we simply write:

SELECT * FROM `Team calendar`

Result

EventDate EndDate fRecurrence Attachments WorkspaceLink Title Location Description fAllDayEvent
2011-02-08 15:00:00 2011-02-08 17:00:00 False False False Meeting 1 Linköping NULL False
2011-02-09 06:00:00 2011-02-09 09:00:00 False False False Meeting 2 Stockholm NULL False
2011-02-11 14:00:00 2011-02-11 18:00:00 False False False Meeting 3 Stockholm NULL False
2011-02-13 01:00:00 2011-02-14 00:59:00 False False False Meeting 4 Oslo NULL True

As you may see the query only returns the first four rows, which also happens to be entries placed on February. This is because the default view of the calendar has a filter on it. The filter within a list or view can be overridden by either providing your own where statement or by setting the connection string option NoListFilters to True.

Now, suppose we want to select all entries shown in the “All Events” view. This is simply done by adding the name of the view to the list name delimited by a dot character.

SELECT * FROM `Team calendar`.`All Events`

Result

fRecurrence Attachments WorkspaceLink LinkTitle Location EventDate EndDate fAllDayEvent
False False False Meeting 1 Linköping 2011-02-08 15:00:00 2011-02-08 17:00:00 False
False False False Meeting 2 Stockholm 2011-02-09 06:00:00 2011-02-09 09:00:00 False
False False False Meeting 3 Stockholm 2011-02-11 14:00:00 2011-02-11 18:00:00 False
False False False Meeting 4 Oslo 2011-02-13 01:00:00 2011-02-14 00:59:00 True
False False False Meeting 5 NULL 2011-03-09 14:00:00 2011-03-09 15:00:00 False
False False False Meeting 6 Linköping (Platensgatan) 2011-03-15 14:00:00 2011-03-15 17:00:00 False

In this case all rows are returned since there is no filter defined on the “All Events” view. As you can see, this view also has a different set of columns from the default view.

In the next example we will be a little more specific in our query and only ask for columns “Title”, “Location”, “EventDate” and “EndDate”. Note that LinkTitle and Title return same values. When specific columns are selected all the columns defined in the view are omitted.

SELECT Title, Location, EventDate, EndDate FROM `Team calendar`

Result

Title Location EventDate EndDate
Meeting 1 Linköping 2011-02-08 15:00:00 2011-02-08 17:00:00
Meeting 2 Stockholm 2011-02-09 06:00:00 2011-02-09 09:00:00
Meeting 3 Stockholm 2011-02-11 14:00:00 2011-02-11 18:00:00
Meeting 4 Oslo 2011-02-13 01:00:00 2011-02-14 00:59:00

Where

Filtering becomes really easy with Camelot .NET Connector. The following lists the comparison operators available within the where syntax:

Operator Description
= Equal to
<> Not equal to
< Less than
<= Less than or equal to
> greater than
>= greater than or equal to
LIKE Used to compare strings
NULL Is null
NOT NULL Is not null

The listed operators can be used in any combinations with AND/OR keywords to define complex conditions. Suppose that we need to select all entries starting from 9th of February at 17:00 but we want to exclude all day events.

SELECT Title, Location, EventDate, EndDate FROM `Team calendar` WHERE EventDate > ’2011-02-09 17:00:00′ AND fAllDayEvent = False

Result

Title Location EventDate EndDate
Meeting 3 Stockholm 2011-02-11 14:00:00 2011-02-11 18:00:00
Meeting 5 NULL 2011-03-09 14:00:00 2011-03-09 15:00:00
Meeting 6 Linköping (Platensgatan) 2011-03-15 14:00:00 2011-03-15 17:00:00

The query results in three rows. Note that the connector allows some various date formats, that can be found in the related documentation.

In the next example we want to select all calendar events with no location set.

SELECT Title, Location, EventDate, EndDate FROM `Team calendar` WHERE Location IS NULL

Result

Title Location EventDate EndDate
Meeting 5 NULL 2011-03-09 14:00:00 2011-03-09 15:00:00

Results in a single row.

Today() and Now() are special functions that can be useful when dealing with times. For example, if we want to select all events occurring today, then we would simply write:

SELECT Title, Location, EventDate, EndDate FROM `Team calendar` WHERE EventDate = Today()

The UserId() function is useful in scenarios where you need to make use of the current user in your query, i.e. the user through which the connector is executing. For example, if we need to list all events created by the curent user we can simply write:

SELECT Title, Location, EventDate, EndDate FROM `Team calendar` WHERE Author = UserId()

Order By

The order by statement is used to order the results by one or more columns. The following simple query shows how to order calendar events first by location and secondly by the start time of the events. The default sort order is ascending if not specified closer using the ASC or DESC keywords.

SELECT Title, Location, EventDate, EndDate FROM `Team calendar` ORDER BY Location, EventDate

Result

Title Location EventDate EndDate
Meeting 5 NULL 2011-03-09 14:00:00 2011-03-09 15:00:00
Meeting 1 Linköping 2011-02-08 15:00:00 2011-02-08 17:00:00
Meeting 6 Linköping (Platensgatan) 2011-03-15 14:00:00 2011-03-15 17:00:00
Meeting 4 Oslo 2011-02-13 01:00:00 2011-02-14 00:59:00
Meeting 2 Stockholm 2011-02-09 06:00:00 2011-02-09 09:00:00
Meeting 3 Stockholm 2011-02-11 14:00:00 2011-02-11 18:00:00

Limit

Finally, we can choose to limit the number of events returned by adding a limit clause to our query. The limit clause overrides the default limit, which is specified in the connection string through the DefaultLimit option. If this option is not set, then the query returns the number of rows specified on the view in SharePoint.

SELECT Title, Location, EventDate, EndDate FROM `Team calendar` ORDER BY Location, EventDate LIMIT 1

Posted in SharePoint | Leave a comment

Camelot .NET Connector and SqlDataSource

One of the most common examples of  databinding in ASP.NET is how to use the SqlDataSource control to connect to an underlying SQL data source. This is also a great way to show the simpleness that comes with the Camelot .NET Connector for Microsoft SharePoint and integration with MOSS/WSS. The SqlDataSource control uses ADO.NET classes to interact with any database supported by ADO.NET.

When you configure the SqlDataSource control, you use the ProviderName property to identify the database that you want to connect to (in our case Camelot.SharePointConnector) and the ConnectionString property to set the connection string required to connect to MOSS/WSS.

Example: Using the connector with SqlDataSource to populate a DropDownList

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
  <head id="Head1" runat="server">
    <title>ASP.NET DropDown Example</title>
</head>
<body>
    <form id="form2" runat="server">

      <asp:SqlDataSource
          id="SqlDataSource1"
          runat="server"
          ProviderName="Camelot.SharePointConnector"
          DataSourceMode="DataReader"
          ConnectionString="Server=demo.bendsoft.com;Domain=;User=spuser;Password=sppwd;Authentication=Ntlm;"
          SelectCommand="SELECT ID, Name FROM Employees">
      </asp:SqlDataSource>

      <asp:DropDownList
          id="ListBox1"
          runat="server"
          DataValueField="ID"
          DataTextField="Name"
          DataSourceID="SqlDataSource1">
      </asp:DropDownList>

    </form>
  </body>
</html>

Posted in SharePoint | Leave a comment

Integrating SharePoint and Umbraco

Umbraco is a really cool open source CMS released under MIT license. And with more than 85000 active installations I must say it’s a very impressing piece of work. Microsoft Office SharePoint Server (MOSS) and Windows SharePoint Services (WSS) is quickly becoming a dominate application for storing and maintaining enterprise data and intelligence for organizations of all sizes.

But, they don’t do together. There are a few ways of integrating and interfacing towards SharePoint but each and one of them requires you to either spend a lot of time understanding the MOSS/WSS data structure, naming conventions and data type conversion requirements. The developer also needs to understand the CAML (Collaborative Application Markup Language) syntax used to query lists and views. The code base tends to grow to a size that is not in proportion to the functionality.

This is a problem we used to have as well.

Watch the webcast in how to make a simple integration between SharePoint and Umbraco

In this demo we simply pull the data from a SharePoint list to a specific Umbraco template. The data is pulled every time some one requests the page.


http://www.youtube.com/watch?v=Xku09lxPaxA

Tutorial

Setting up SharePoint

  1. Create a list called something like “SharePoint Articles”
  2. Create two extra columns, Article and Publish to external
    1. Article: Use “Multiple lines of text” and uncheck the “Add to default view”
    2. Publish to external: Use “Yes/No (check box)”, make sure the “Add to default view” is checked
  3. Create some articles

When you are done it should look something like this

Building the Umbraco macro

Create a standard ASP.NET Web Application in Visual Studio 2010, throw away everything except the project file and create a “New Item” > “Web User Control”. I named mine to UmbracoCamelotDemo.

UmbracoCamelotDemo.ascx

This is the file that renders the content to Umbraco. You should have one line in it, the <%@ Control. We must add an asp:Panel to display our data.

<%@ Control Language="vb" AutoEventWireup="false" CodeBehind="UmbracoCamelotDemo.ascx.vb" Inherits="UmbracoCamelotDemo.WebUserControl1" %>
<asp:Panel ID="Panel1" runat="server"></asp:Panel>

UmbracoCamelotDemo.ascx.vb

The code behind file …

Public Class WebUserControl1
    Inherits System.Web.UI.UserControl

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

        Dim connectionString As String = "Server=yourserver;Database=;Domain=;User=sharepointuser;"_ "Password=sharepointpassword;Authentication=Ntml;TimeOut=10;strictmode=false;recursivemode=recursiveall;"_ "defaultlimit=1000;cachetimeout=5;MaxReceivedMessageSize=10000000;MaxBytesPerRead=10000000"
        Dim conn As New Camelot.SharePointConnector.Data.SharePointConnection(connectionString)

        Dim query As String = "SELECT ID, Title, Article FROM `SharePoint articles`.all WHERE `Publish to external` = 1"
        Dim adapter As New Camelot.SharePointConnector.Data.SharePointDataAdapter(query, conn)

        Dim dtMyWebPartList As New DataTable()
        adapter.Fill(dtMyWebPartList)

        For Each row As DataRow In dtMyWebPartList.Rows
            Dim c As New System.Web.UI.WebControls.Label
            c.Text = "<h2>" + row.Item("Title").ToString + "</h2>"
            c.Text += row.Item("Article").ToString
            Panel1.Controls.Add(c)
        Next


    End Sub

End Class

Build, deploy and show

Simply build the files and deploy them in Umbraco

  • Put UmbracoCamelotDemo.ascx in the /usercontrols directory in the Umbraco web root
  • Put the UmbracoCamelotDemo.ascx.vb file in the /bin directory in the Umbraco web root.

Add the macro in the Umbraco admin interface

  1. Navigate to the Developer section
  2. Right click Macros and select create
  3. Type CamelotDemo as name (Or what ever suits you best, this can be changed afterwards)
  4. Use the list “Browse usercontrols on server…” and select your usercontrol there.
  5. Save

Navigate to the settings section and create a new template, or modify an existing. This example creates a new template

  1. Right click the Master page and select Create
  2. Add any elements you prefer here and the macro, I’ve posted my page code below. To add a macro click the macro button and select your macro
  3. Save

<%@ Master Language="C#" MasterPageFile="~/masterpages/RunwayMaster.master" AutoEventWireup="true" %>
<asp:Content ContentPlaceHolderID="RunwayMasterContentPlaceHolder" runat="server">
  <div id="content">
    <umbraco:Item runat="server" field="bodyText" />
    <umbraco:Macro Alias="CamelotDemo" runat="server"></umbraco:Macro>
  </div> 
</asp:Content>?

You must also add the new template to a document type

  1. Expand the document types and select the document type you want to link i to (or allow it from)
  2. Save

Create the page SharePoint Articles

  1. Navigate to the content section
  2. Create a new page, name it and select the document type you linked your template to
  3. Enter some body text if you like and go to the properties tab
  4. Select the template you created
  5. Save and publish

Thats it!

You should now have a live view of your articles stored in SharePoint in Umbraco

Thanks for reading, please leave a comment or contact us at info@bendsoft.com

Posted in SharePoint | Leave a comment