Thursday 13 October 2011

Building a Document Management System with SharePoint 2010 - Part 11–Creating Content Types from the Client Model

This post is more about how to improve the performance of your DMS to the extreme using the client model:
Microsoft.SharePoint.Client.Runtime.dll
Microsoft.SharePoint.Client.dll
Well, what I mean with best performance is to be able to not just create Content Types with the Client Model (I don’t cover this part here because I assume you already have the content types), it is the fact we can assign Content Types, something that is not allowed in the client model unless you use some tricks.

What I am going to do is to assign two different Content Types (Client Folder, Matter Folder) so we can have a client/matter structure in our DMS. Both Content Types are already created and inherits from the native folder Content Type. So we will have something like this:


+Client1----+Matter1
                   |
                   +Matter2
                   |
                   +Matter3

Before going ahead wit the chunk of code, I will explain you how it works.
1- You create your folder.
2- You get the id from your content type.
3- You apply the id into this field: “ContentTypeId”. I update the client number and client description.

ListItem item = items[0];
item["ContentTypeId"] = _ctClientFolder.Id;
item["client"] = _sClientNumber;
item["clientdescription"] = _sClientDescription;
item.Update();
clientContext.ExecuteQuery();



I am going to add the code below, to use it, be sure you add:
Microsoft.SharePoint.Client.Runtime.dll
Microsoft.SharePoint.Client.dll
Into your references and :

using Microsoft.SharePoint.Client;
To call the method just try this:
CreateClientMatter(@"http://mysharepointserver", "Documents", "00000272", "my client", "00000018", "my matter");
Just copy and paste this code into your project, if the client/matter is not there it will created and add the properties:
        private List ListCheck(ClientContext clientContext, Web _wListWeb, string _sListName)
        {
            List _lExistingList = null;
            try
            {
                //###############################
                //## We check if the file exists
                //###############################
                _wListWeb = clientContext.Web;
                ListCollection _lCollectionOfLists = _wListWeb.Lists;
                IEnumerable<List> existingLists = clientContext.LoadQuery(
                        _lCollectionOfLists.Where(
                        list => list.Title == _sListName)
                        );
                clientContext.ExecuteQuery(); 
                _lExistingList = existingLists.FirstOrDefault();
                //############################### 
            }
            catch (Exception ex)
            {
                throw new ArgumentNullException();
            }
            return _lExistingList;
        }
        private void CreateClientFolder(string _sServerURL, ClientContext clientContext, Web _wListWeb, List _lList, string _sListName, string _sClientNumber, string _sClientDescription)
        {
            //## Variables.
            Folder _fExistingFolder = null;
            ContentType _ctClientFolder = null; 
            //## We get all the folders
            FolderCollection _afolders = _lList.RootFolder.Folders; 
            //## Building the URL (folder)
            String _sClientfolderUrl = String.Format("/{0}/{1}", _sListName, _sClientNumber);
            //## We check if the URL exists
            IEnumerable<Folder> _iClientExistingFolders = clientContext.LoadQuery<Folder>(
                _afolders.Where(
                folder => folder.ServerRelativeUrl == _sClientfolderUrl)
                );
            clientContext.ExecuteQuery();
            //## Getting the result
            _fExistingFolder = _iClientExistingFolders.FirstOrDefault();
            if (_fExistingFolder == null)                       
            {
                //## Adding the folder
                _wListWeb.Folders.Add(_sServerURL + @"/" + _sListName + @"/" + _sClientNumber);
                clientContext.ExecuteQuery();
                //## Quering our folder
                CamlQuery query = new CamlQuery();
                query.ViewXml = @"       <View>
                                             <Query>
                                                <Where>
                                                   <Eq>
                                                      <FieldRef Name='FileLeafRef'/>
                                                      <Value Type='Text'>"+_sClientNumber+@"</Value>
                                                   </Eq>
                                                </Where>
                                             </Query>
                                             <RowLimit>1</RowLimit>
                                          </View>"; 
                ListItemCollection items = _lList.GetItems(query);
                clientContext.Load(items);
                //## Getting the content types
                ContentTypeCollection _ctListOfContentTypes = clientContext.Web.AvailableContentTypes;
                clientContext.Load(_ctListOfContentTypes);
                clientContext.ExecuteQuery();
                //## Getting the Content Type Id
                foreach (ContentType _ctItem in _ctListOfContentTypes)
                {
                    if (_ctItem.Name == "Client Folder")
                    {
                        _ctClientFolder = _ctItem;
                        break;
                    }
                }
                //## This is the tricky part, where we convert our folder
                //## Into a content type. We add some metadata as well               
                if (items.Count > 0)
                {
                    ListItem item = items[0];
                    item["ContentTypeId"] = _ctClientFolder.Id;
                    item["client"] = _sClientNumber;
                    item["clientdescription"] = _sClientDescription;
                    item.Update();
                    clientContext.ExecuteQuery();
                }
            }
        }
 
        private void CreateMatterFolder(string _sServerURL, ClientContext clientContext, Web _wListWeb, List _lList, string _sListName,string _sClientNumber, string _sMatterNumber,string _sMatterDescription)
        {
            Folder _fExistingFolder = null;
            ContentType _ctMatterFolder = null; 
            FolderCollection _afolders = _lList.RootFolder.Folders; 
            String _sMatterfolderUrl = String.Format("/{0}/{1}/{2}", _sListName, _sClientNumber, _sMatterNumber); 
            IEnumerable<Folder> _iMatterExistingFolders = clientContext.LoadQuery<Folder>(
                _afolders.Where(
                folder => folder.ServerRelativeUrl == _sMatterfolderUrl)
                );
            clientContext.ExecuteQuery();
            _fExistingFolder = _iMatterExistingFolders.FirstOrDefault(); 
            if (_fExistingFolder == null)
            {
                _wListWeb.Folders.Add(_sServerURL + @"/" + _sListName + @"/" + _sClientNumber + @"/" + _sMatterNumber);
                clientContext.ExecuteQuery();
                //## Quering our folder
                CamlQuery query = new CamlQuery();
                query.ViewXml = @"       <View Scope='RecursiveAll'>
                                             <Query>
                                                <Where>
                                                   <Eq>
                                                      <FieldRef Name='FileLeafRef'/>
                                                      <Value Type='Text'>" + _sMatterNumber + @"</Value>
                                                   </Eq>
                                                </Where>
                                             </Query>
                                             <RowLimit>1</RowLimit>
                                          </View>";
 
                ListItemCollection items = _lList.GetItems(query);
                clientContext.Load(items); 
                //## Getting the content types
                ContentTypeCollection _ctListOfContentTypes = clientContext.Web.AvailableContentTypes;
                clientContext.Load(_ctListOfContentTypes);
                clientContext.ExecuteQuery(); 
                //## Getting the Content Type Id
                foreach (ContentType _ctItem in _ctListOfContentTypes)
                {
                    if (_ctItem.Name == "Matter Folder")
                    {
                        _ctMatterFolder = _ctItem;
                        break;
                    }
                }
                //## This is the tricky part, where we convert our folder
                //## Into a content type. We add some metadata as well               
                if (items.Count > 0)
                {
                    //## This is just in case the client number
                    //## has the same name than the new matter
                    ListItem item = items.Count == 2 ? items[1] : items[0];
                    item["ContentTypeId"] = _ctMatterFolder.Id;
                    item["matter"] = _sMatterNumber;
                    item["matterdescription"] = _sMatterDescription;
                    item.Update();
                    clientContext.ExecuteQuery();
                }
            }
        }
 
        public bool CreateClientMatter(string _sServerURL, string _sListName, string _sClientNumber, string _sClientName, string _sMatterNumber, string _sMatterName)
        {
            bool _bResult = false;
            List _lList = null;           
            try
            {
                using (var clientContext = new ClientContext(_sServerURL))
                {
                    Web _wListWeb = clientContext.Web;
                    //###############################
                    //## We check if the list exists
                    //###############################                   
                    _lList = ListCheck(clientContext, _wListWeb, _sListName);
                    //###############################                  
                    if (_lList != null)
                    {                       
                        //#################################
                        //## CREATE FOLDER CONTENT TYPES
                        //## IF THEY DON'T EXIST
                        //##################################
                        CreateClientFolder(_sServerURL, clientContext, _wListWeb, _lList, _sListName, _sClientNumber, _sClientName);
                        CreateMatterFolder(_sServerURL, clientContext, _wListWeb, _lList, _sListName, _sClientNumber,_sMatterNumber,_sMatterName);                    
                        //##################################
                        _bResult = true;
                    }
                }
            }
            catch (Exception ex)
            {
                string _sError = ex.ToString();               
            }
            return _bResult;
        }


Conclusion: In order to create a decent DMS be always sure you keep your server clean of web services and workflows, anything it can be done in the client side, it will leave the server free to do what it has to do, SERVE!

Wednesday 5 October 2011

Updating properties when you save a file from an external program and properties.ListItem.SystemUpdate();

The other day I came across, by accident, with one of my worst nightmares, update the properties of a file when the file is uploaded by an external program (Non Office 2010), in fact it was Adobe Acrobat Reader.

After spending a couple of hours trying to figure out how Adobe Reader works with Sharepoint 2010, I found out that before saving the file it does a checkin so it can add all the properties.

My first approach was to develop an Event Receiver and go ItemAdded(…) so I could add the properties without any problem. When I tried that the item was updated BUT Adobe Reader throw an error saying that the file didn’t exist. Quite weird. So I assumed that Adobe decided to do that because it lost control of the file, at the end of the day I was modifiying the properties with my lovely Event Receiver.

To avoid that error I decided to go to the ItemCheckedIn(…) event and modify my properties, so Adobe should be happy. I used the elegant method properties.ListItem.SystemUpdate() to update the properties, otherwise I was going to receive a similar error, well, it is the first time I use this method, but it worked really well.

properties.ListItem.SystemUpdate(): Updates the database with changes that are made to the list item without changing the Modified or Modified By fields.

Some important notes on using Update vs SystemUpdate.
For Update:  Once you update, the modified by and modified dates change to when the code execute and may show the item as being last modified by "System Account".  The code also works with the item like it has nothing at all to do with the previous version.  User updates, then Code updates.

For SystemUpdate(Optional Boolean value)
If you are stuck trying to update a field for a document library, please be aware of two things in Sharepoint 2007/WSS 3.0.
1. Even if you are NOT using Checked out/checked in, the document still has a SPCheckOutStatus.ShortTerm attached to it for a brief period of time.  Trying to SPListItem.Update or SPFile.Item.Update at this point will likely tell you that the file is locked for editing.  There is a .checkoutexpires property to check if you absolutely must wait for the item to be checked back in.  (e.g. Are they really done with that darn thing?)
2. Even if you do NOT have versioning turned on, you are, in fact, working with a new version of the document.  Use SPListItem.SystemUpdate(false) in order to bypass a short term locked document.

Full Example:
file.Item["Status"] = "Pending";
file.Item.Web.AllowUnsafeUpdates = true;
file.Item.Update() //fails miserably on ItemChanged due to the SPCheckOutStatus.ShortTerm lock.
file.Item.SystemUpdate(false); //This works while the user still has the document open. 
file.Item.Web.AllowUnsafeUpdates = false;

I attach the code just ion case you want to deliver solutions conected to external programs. Remember that this code comes from an Event Receiver:

/// <summary>
/// An item was checked in
/// </summary>
public override void ItemCheckedIn(SPItemEventProperties properties)
{
   try
   {              
      //## Propperty to be updated
      properties.ListItem["client"] = "222222";
      //## Updating with systemUpdate()
      properties.ListItem.SystemUpdate();        
   }
   catch (Exception ex)
   {
      Microsoft.SharePoint.Administration.SPDiagnosticsService diagSvc = Microsoft.SharePoint.Administration.SPDiagnosticsService.Local;
      diagSvc.WriteTrace(0, new SPDiagnosticsCategory("PDFFileUpdater", TraceSeverity.Monitorable, EventSeverity.Error), TraceSeverity.Monitorable, "Writing to the ULS log:  {0}", new object[] { ex.ToString() });
   }
   base.ItemCheckedIn(properties);
}

Tuesday 4 October 2011

Sending an Email with Attachments in Sharepoint 2010

As you probably know Sharepoint provides a mechanism to send emails to users. The class that Microsoft provides for this task is called Microsoft.Sharepoint.Utilities.SPUtility and the method is called SendEmail(…).

I have to admit, this class works really well, in fact it always delivers the emails BUT there is something that it can’t do for you. Something at some point you will need, attachments. SPUtility can’t send any attachments so if at some point you want to send an appointment or file, you will not be able to do it.

What is the solution, well, because Sharepoint 2010 works under .NET 3.5 in a “ASP.NET” environment there is a nice class we can use to send attachments: System.Net.Mail.SmtpClient .

I am going to post a piece of code with a simple appointment attached to the email. I am going to do this exercise because there are plenty of examples of this class out there (files or lines of text).

I have a button in a form called CMDSendEmail, when you click it will send the email:

private void CMDSendEmail_Click(object sender, EventArgs e)
{
      string _sCalendar = null;
      _sCalendar = BuildEventCalendar( DateTime.Now, 
                                DateTime.Now,
                                DateTime.Now, 
                                "Park", 
                                "Sharepoint Discussion", 
                                "More discussion", 
                                "organaizer@email.com");
      SendSharepointEmailWithCalendarEventNetClass(
                               "from@email.com", 
                               "to@email.com",
                               "mysubject", 
                               "mybody", 
                               _sCalendar, 
                               "myevent.ics", 
                               false, 
                               true);
}
private bool SendSharepointEmailWithCalendarEventNetClass(string _sFrom, string _sTo, string _sSubject, string _sBody, string _bAttachemnt, string _sAttachmentName, bool _bIsFromLocal, bool _bDoesItHaveAttachment)
{
            bool _bResult = false;
            try
            {
                //Get the Sharepoint SMTP information from the SPAdministrationWebApplication
                string smtpServer = SPAdministrationWebApplication.Local.OutboundMailServiceInstance.Server.Address;
                string smtpFrom = _bIsFromLocal ? SPAdministrationWebApplication.Local.OutboundMailSenderAddress : _sFrom;
                //Create the mail message and supply it with from and to info
                MailMessage mailMessage = new MailMessage(smtpFrom, _sTo);
                //Set the subject and body of the message
                mailMessage.Subject = _sSubject;
                mailMessage.Body = _sBody;
                mailMessage.IsBodyHtml = true;
                if (_bDoesItHaveAttachment)
                {
                    //## Create the attachment passing the parameters
                    System.Net.Mail.Attachment _mAttachment = 
                        System.Net.Mail.Attachment.CreateAttachmentFromString(
                        _bAttachemnt,
                        _sAttachmentName,
                        System.Text.Encoding.Unicode,
                        "text/calendar");
                   
                    //##Add the attachment
                    mailMessage.Attachments.Add(_mAttachment);
                }
                //##Create the SMTP client object and send the message
                SmtpClient smtpClient = new SmtpClient(smtpServer);
                smtpClient.Send(mailMessage);
                _bResult = true;
            }
            catch (Exception ex)
            {
                _bResult = false;
            }
            return _bResult;
}
public string BuildEventCalendar(DateTime _dStart, DateTime _dEnd, DateTime _dDateStamp, string _sLocation, string _sSummary, string _sDescription, string _sOrganizer)
{
            System.Text.StringBuilder sbICSFile = new System.Text.StringBuilder();
            
            sbICSFile.AppendLine("BEGIN:VCALENDAR");
            sbICSFile.AppendLine("PRODID:-//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN");
            sbICSFile.AppendLine("VERSION:2.0");
            sbICSFile.AppendLine("METHOD:PUBLISH");
            sbICSFile.AppendLine("BEGIN:VEVENT");
            sbICSFile.AppendLine("ORGANIZER:MAILTO:"+_sOrganizer);
            sbICSFile.AppendLine("DTSTART:" + _dStart.ToUniversalTime().ToString("yyyyMMdd\\THHmmss\\Z"));
            sbICSFile.AppendLine("DTEND:" + _dEnd.ToUniversalTime().ToString("yyyyMMdd\\THHmmss\\Z"));
            sbICSFile.AppendLine("LOCATION:" + _sLocation);
            sbICSFile.AppendLine("TRANSP:OPAQUE");
            sbICSFile.AppendLine("SEQUENCE:0");
            sbICSFile.AppendLine("UID:" + DateTime.Now.Ticks.ToString());
            sbICSFile.AppendLine("DTSTAMP:" + _dDateStamp.ToUniversalTime().ToString("yyyyMMdd\\THHmmss\\Z"));
            sbICSFile.AppendLine("SUMMARY:" + _sDescription);
            sbICSFile.AppendLine("PRIORITY:5");
            sbICSFile.AppendLine("X-MICROSOFT-CDO-IMPORTANCE:1");
            sbICSFile.AppendLine("CLASS:PUBLIC");
            sbICSFile.AppendLine("END:VEVENT");
            sbICSFile.AppendLine("END:VCALENDAR");
            return sbICSFile.ToString();
}


As you can see it is pretty simple you create the Event and attach it as "text/calendar" type.
I have only created one attachment, but I recommend you to try more types, to see how smooth the system is.


Conclusion: Even if Sharepoint 2010 doesn’t have the functionality to send attachments on emails, the beautiful .NET 3.5 can handle that for you in an elegant way.

Monday 3 October 2011

Embedding Google charts into Sharepoint Web Parts

The other day I was developing a portal and at some point the “client” asked to provide a simple pie chart in the landing page. I said to him; “that should be ok”. At that point I was thinking obviously in the lovely Chart Web Part that comes from default in Sharepoint 2010. After talking with him for a while I realised that requirements where quite high, in terms of data collection. I want to clarify I am trying to avoid PerformancePoint Services because I don’t want to activate the service just for one chart.

Obviously I always have plan A,B,C,D…well sometimes I reach Z. I tried to play around a little bit with the Out Of The Box web part, unfortunately plan A was not good enough, I was looking for something sharp, something elegant, something independent from the whole system, and going for plan A should require to create few extra workflows.

Then I tried to create a Visual Webpart, thinking that the System.Web.UI chart control will work properly in Sharepoint 2010. Unfortunately, it didn’t. Big mistake in my side, I was playing with a .NET 4.0 control, obviously Sharepoint 2010 only support .NET 3.5, so I had to abort plan B.

I asked Trev, one of the web developers, and he suggested me Google Charts. In fact he said that they look much better than the .NET ones, because they use AJAX. I went to the Google Chart Tools website and I was amazed about what I saw. Google provides all the information about the API + all the code required, so with a copy paste you can get a nice example in five minutes.

What I did was to create a nice method where you can pass the parameters , build the JavaScript code and embedded into the Web Part. You can use, either a Web Part or a Visual Web Part. In my case I decided to use a Visual Web Part. There is not a particular reason for that.

I am going to do a Step by Step, so you know how to implement it. The result will be a simple pie chart (remember you can create something similar with another Google chart). I will post the project at the end of the article so you can implement it.

- Step 1
Go to Visual Studio 2010->New Project->Sharepoint->2010->Empty Project->call it CPDPointsWebPart and click OK. Select you want to deploy in your farm.

- Step 2
Go to the project, Right click and add->new item…->Sharepoint->2010->Visual Webpart and call it CPDWebPart.

- Step 3
Go to the Feature (Feature1)-> Right click and rename it to CPDWebPartFeature. Your solution should look like this now:
image

- Step 4
Go to CPDWebPartUserControl.ascx.cs, remove all the code and paste this one:

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Collections.Generic;
namespace CPDPointsWebPart.CPDWebPart
{
    public partial class CPDWebPartUserControl : UserControl
    {       
        protected void Page_Load(object sender, EventArgs e)
        {
            
            Dictionary<string, int> _dValues = new Dictionary<string, int>();
            _dValues.Add("CPD Training done", 4);
            _dValues.Add("CPD Training Left to be done", 6);
            
            List<string> _lColumns = new List<string>();
            _lColumns.Add("CPD");
            _lColumns.Add("Hours");
            
            Page.RegisterClientScriptBlock("PieChart", GooglePieChart("CPD Points",_dValues,_lColumns,450,300));
        }        
        public string GooglePieChart(string _sTitle, Dictionary<string, int> _dChartParameters, List<string> _lColumns, int _iWidth, int _iHeight)
        {
            System.Text.StringBuilder _Chart = new System.Text.StringBuilder();
            _Chart.Append(@"<script type='text/javascript' src='https://www.google.com/jsapi'></script>");
            _Chart.Append(@"<script type='text/javascript'> ");
            _Chart.Append(@"google.load('visualization', '1', {packages:['corechart']}); ");
            _Chart.Append(@"google.setOnLoadCallback(drawChart);");
            _Chart.Append(@"function drawChart() {  ");
            _Chart.Append(@"var data = new google.visualization.DataTable();  ");
            _Chart.Append(@"data.addColumn('string', '" + _lColumns[0] + "');");
            _Chart.Append(@"data.addColumn('number', '" + _lColumns[1] + "');");
            _Chart.Append(@"data.addRows(" + _dChartParameters.Count.ToString()+ ");");
    
            int i=0;
            foreach (var item in _dChartParameters)
         {
          _Chart.Append(@"data.setValue("+i.ToString()+", 0, '"+item.Key+"'); ");
                _Chart.Append(@"data.setValue(" + i.ToString() + ", 1, " + item.Value+ "); ");   
                i++;
         }
            _Chart.Append(@"var chart = new google.visualization.PieChart(document.getElementById('chart_div'));");
            _Chart.Append(@"chart.draw(data, {width: " + _iWidth.ToString() + ", height: " + _iHeight.ToString() + ", title: '" + _sTitle + "'}); ");
            _Chart.Append(@"}");
            _Chart.Append(@"</script>");
            return _Chart.ToString();
        }
    }             
}


Notice I use Page.RegisterClientScriptBlock(…) to post the JavaScript code into the Visual Web Part.


- Step 5
Double click on CPDWebPartUserControl.ascx and copy and paste this code. The only line I am going to add is “<div id="chart_div"></div> “ . You can add this line from your code as well, but I think by doing it like this we can see the interaction between the JavaScript code and the ascx code.


- Step 6
Deploy the Visual Web Part, go to your site edit the page, insert the webpart (it will be under custom). This should be the result:
image


Conclusion: Google can provide a very good solution for your charts. There are more chart engines out there you can use, but I find Google the fastest one in terms of performance.


To download the code click in the image.
image