Second Life of a Hungarian SharePoint Geek

April 27, 2010

A simple demo application of Office 2010 and SharePoint 2010 development using Visual Studio 2010

In the past few weeks I had not much time for blogging. Beyond my general daily work and a few strict deadlines I was preparing for my presentation about VS 2010 based SharePoint and Office 2010 development. To compensate the low number of blog posts in the last month I would like to share the demo application with you.

This demo was presented at the Hungarian Visual Studio 2010 Launch event on 12th April 2010. The post is rather long, but I hope it is worth to read. You find the sample code here, so you can play with it at your environment if you wish.

There are two VS 2010 solutions in the demo. One for the SharePoint part, and the other one for Office.

Before going to deep into the technical details, let’s see the overview of the solution from a higher point of view. We store the lists of F1 races and F1 drivers in SharePoint lists. Our Excel add-in will connect to the list of F1 drivers and “download” the items to the Excel sheet. After we set the time results for each of the pilots, we can compute the result of the race. Then we can go to the our custom backstage view and select the race we set the results for. Finally we can publish the results back to the server where first a Word document is generated based on the result and it is converted to a PDF document.

After this short introduction, let’s start with the SharePoint solution!

The solution assumes your SharePoint server is called SP2010 and there is a F1Site web for the demo. If you have other configuration, you should alter the Site URL property of the project before deployment.

In this solution we create  two list definitions, one for the F1 pilots (including name of the pilot as Title, nationality as Country Code, Constuctor and Engine fields) and another one for F1 races. This list includes name of the GP as Title, City for the city of the race, RaceDate for the exact date and time of the race, CircuitImage for the image of the circuit (not mandatory, used only in a few races for illustration purposes) and Result field to store the result of the race in XML format.

We create two list instances based on these list definitions to upload the initial data for our application, as shown on the images below:

image

image

Additionally, we create a document library (called DocConverter) to store generated Word documents and converted PDF documents.

I created an event receiver for the F1 Races list to generate a Word document into the DocConverter library when the results are published and another event receiver on the DocConverter to do the conversion when the Word files are generated. More on these event receivers later.

There is a mapped folder for the flag and circuit images we need in the demo.

I’ve included a feature receiver in the solution just to show it is possible so easily in VS 2010. It’s commented out by default in the sample code, feel free to use it if you would like to see it in action:

  1. public override void FeatureActivated(SPFeatureReceiverProperties properties)
  2. {
  3.     //SPWeb web = (SPWeb)properties.Feature.Parent;
  4.     //web.Webs.Add("VS2010Launch", "VS 2010 Launch",
  5.     //    "VS 2010 Launch Web Site", 1033, SPWebTemplate.WebTemplateSTS,
  6.     //    false, false);
  7. }
  8.  
  9.  
  10. // Uncomment the method below to handle the event raised before a feature is deactivated.
  11.  
  12. public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  13. {
  14.     //SPWeb web = (SPWeb)properties.Feature.Parent;
  15.     //web.Webs["VS2010Launch"].Delete();
  16. }

Finally, there is a simple Visual Web Part item in the project that utilizes the data included in the application and illustrates working with LINQ on SharePoint data.

To generate the helper class I used SPMetal:

"C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\BIN\SPMetal" /web:http://SP2010/F1Site /code:F1SiteLinq.cs
/namespace:VS2010Launch.F1Results  /language:csharp /parameters:SPMetal.xml

And this is the configuration file for SPMetal:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Web AccessModifier="Internal" xmlns="http://schemas.microsoft.com/SharePoint/2009/spmetal">
  3.     <List Name="F1 Races">
  4.         <ContentType Name="Item" Class="F1Race">
  5.             <Column Name="City" Member="City" />
  6.             <Column Name="RaceDate" Member="RaceDate" />
  7.             <Column Name="CircuitImage" Member="CircuitImage" />
  8.         </ContentType>
  9.     </List>
  10.     <ExcludeOtherLists />
  11. </Web>

Using the generated helper class accessing our data stored in SharePoint is as simple as this code shows you:

  1. public partial class F1ResultsUserControl : UserControl
  2. {
  3.     protected void Page_Load(object sender, EventArgs e)
  4.     {
  5.         F1SiteLinqDataContext dataContext = new F1SiteLinqDataContext(SPContext.Current.Web.Url);
  6.         EntityList<F1Race> f1Races = dataContext.GetList<F1Race>("F1 Races");
  7.         var allF1Races = from f1Race in f1Races
  8.                             orderby f1Race.Id
  9.                             select f1Race;
  10.  
  11.         bool initialized = false;
  12.         foreach (var race in allF1Races)
  13.         {
  14.             ListItem raceListItem = new ListItem(race.Title, race.Id.ToString());
  15.             if ((!initialized) && (!IsPostBack))
  16.             {
  17.                 initialized = true;
  18.                 SetValues(race);
  19.             }
  20.             Races.Items.Add(raceListItem);
  21.         }            
  22.     }
  23.  
  24.     private void SetValues(F1Race race)
  25.     {
  26.         NoMapLabel.Visible = String.IsNullOrEmpty(race.CircuitImage);
  27.         CircuitImage.Visible = !NoMapLabel.Visible;
  28.         CircuitImage.ImageUrl = String.Format("/F1Site/_layouts/images/VS2010Launch/Circuits/{0}.png", race.CircuitImage);
  29.         City.Text = race.City;
  30.         RaceDate.Text = race.RaceDate.Value.ToString("yyyy.MM.dd HH.mm");
  31.     }
  32.  
  33.     protected void Races_SelectedIndexChanged(object sender, EventArgs e)
  34.     {
  35.         DropDownList racesDropDown = (DropDownList)sender;
  36.         int f1RaceId = int.Parse(racesDropDown.SelectedValue);
  37.         F1SiteLinqDataContext dataContext = new F1SiteLinqDataContext(SPContext.Current.Web.Url);
  38.         EntityList<F1Race> f1Races = dataContext.GetList<F1Race>("F1 Races");
  39.         var selectedRace = (from f1Race in f1Races
  40.                             where f1Race.Id == f1RaceId
  41.                             orderby f1Race.Id
  42.                             select f1Race).First();
  43.         SetValues(selectedRace);
  44.     }
  45. }

After deploying our project to SharePoint we have to add the web part to a web part page first, as shown below:

image

The following figure shows the web part in action. Here you can select the race from the drop down list, and the city, date and image (if there is one) are updated based on the selection.

image

Let’s switch to our Office sample project that is based on the Excel 2010 Add-in project template, to see it in action and to check some of its most interesting parts in the implementation.

The goal of the project was to illustrate the UI extensibility (both the “classical” ribbon and the new backstage view) of the Office applications from code and to show how easy it is to get and publish data in Office applications from and to SharePoint 2010.

I’ve added a Ribbon (XML) item to the project, and customized both the code and the XML definition.

When starting the project, the new ribbon item looks like this:

image

In the ribbon the flag and helmet icons are stored as resource images in the project and provided through the following method in the code:

  1. public System.Drawing.Image GetImage(IRibbonControl control)
  2. {
  3.     if (control.Id == "getPilotsButton")
  4.     {
  5.         return Properties.Resources.Helmet;
  6.     }
  7.     else
  8.     {
  9.         return Properties.Resources.Flag;
  10.     }
  11. }

The method is bound to the buttons through the getImage attribute in the XML file:

  1. <button id="getPilotsButton" size="large" label="Get Pilots" screentip="Gets the list of pilots from the SharePoint site" getImage="GetImage" onAction="GetPilotsButton_OnAction" />
  2. <button id="computeResultsButton" getEnabled="IsConnected" size="large" label="Compute Results" screentip="Compute the order" getImage="GetImage" onAction="ComputeResultsButton_OnAction" />

To access SharePoint data we should first specify the location of data and credential we would like to use on connection. If you check the Ask for credential checkbox, you can set connection parameters in the following dialog box when you press the Get Pilots button to populate worksheets with F1 driver data.

image

Connection parameters are stored as user scoped settings. The Password is stored encoded using a helper method defined in our static Util class and decoded when its value is requested by the code.

  1. [global::System.Configuration.UserScopedSettingAttribute()]
  2. [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
  3. [global::System.Configuration.DefaultSettingValueAttribute("Password")]
  4. public string Password {
  5.     get {
  6.         return (Util.Decrypt((string)(this["Password"])));
  7.     }
  8.     set {
  9.         this["Password"] = Util.Encrypt(value);
  10.     }
  11. }

The GetCredentials method is called several times from the code to get the credentials we specified for the connection:

  1. private ICredentials GetCredentials()
  2. {
  3.     if (Properties.Settings.Default.CurrentCredentials)
  4.     {
  5.         return CredentialCache.DefaultCredentials;
  6.     }
  7.     else
  8.     {
  9.         String userName = Properties.Settings.Default.UserName;
  10.         String domainName = String.Empty;
  11.         int pos = userName.IndexOf(@"\");
  12.         if (pos > -1)
  13.         {
  14.             domainName = userName.Substring(0, pos);
  15.             userName = userName.Substring(pos + 1);
  16.         }
  17.         return new NetworkCredential(
  18.             userName,
  19.             Properties.Settings.Default.Password,
  20.             domainName
  21.             );
  22.     }
  23. }

I’ve accessed SharePoint data from the Excel add-in using the REST service of SharePoint as discussed in this post of Randy Williams. As described there, if you work with SharePoint beta 2, you should install ADO.NET Data Services v1.5 CTP2.

Using the F1 Pilots SharePoint list as data source the code that populates the Excel cells with data looks like this:

  1. public void GetPilotsButton_OnAction(IRibbonControl control)
  2. {
  3.     DialogResult result = DialogResult.OK;
  4.     // we ask for credentials only if the user would like to change the stored values
  5.     if (_askForCredentials)
  6.     {
  7.         ConnectForm connectForm = new ConnectForm();
  8.         result = connectForm.ShowDialog();
  9.     }
  10.     if (result == DialogResult.OK)
  11.     {
  12.         try
  13.         {
  14.             _isConnected = true;
  15.             F1SiteDataContext dataContext = new F1SiteDataContext(
  16.                 new Uri(String.Format(@"{0}/_vti_bin/listdata.svc", Properties.Settings.Default.SiteUrl)));
  17.             dataContext.Credentials = GetCredentials();
  18.             var f1Pilots = from f1Pilot in dataContext.F1Pilots
  19.                             //orderby f1Pilot.Title
  20.                             select new
  21.                             {
  22.                                 f1Pilot.Title,
  23.                                 f1Pilot.CountryCode,
  24.                                 f1Pilot.Constructor,
  25.                                 f1Pilot.Engine
  26.                             };
  27.  
  28.             _f1Races = (IEnumerable<F1RacesItem>)
  29.                                             from f1Race in dataContext.F1Races
  30.                                             //orderby f1Race.RaceDate
  31.                                             select new F1RacesItem
  32.                                             {
  33.                                                 ID = f1Race.ID,
  34.                                                 Title = f1Race.Title,
  35.                                                 City = f1Race.City,
  36.                                                 RaceDate = f1Race.RaceDate,
  37.                                                 CircuitImage = f1Race.CircuitImage
  38.                                             };
  39.             if ((_f1Races != null) && (_f1Races.Count() > 0))
  40.             {
  41.                 _currentRace = _f1Races.ElementAt(0);
  42.             }
  43.  
  44.             _ribbon.InvalidateControl("computeResultsButton");
  45.             _ribbon.InvalidateControl("backstage");
  46.  
  47.             Microsoft.Office.Interop.Excel.Application excelApp = _addIn.Application;
  48.             Workbook workbook = excelApp.ActiveWorkbook;
  49.  
  50.             if (workbook != null)
  51.             {
  52.                 Worksheet worksheet = (Worksheet)workbook.Sheets[1];
  53.                 worksheet.Activate();
  54.                 worksheet.Cells[1, 1] = "Position";
  55.                 worksheet.Cells[1, 2] = "Driver";
  56.                 worksheet.Range["B1:B1"].ColumnWidth = 20;
  57.                 worksheet.Cells[1, 3] = "Nationality";
  58.                 worksheet.Range["C1:C1"].ColumnWidth = 12;
  59.                 worksheet.Cells[1, 4] = "Constructor";
  60.                 worksheet.Range["D1:D1"].ColumnWidth = 20;
  61.                 worksheet.Cells[1, 5] = "Engine";
  62.                 worksheet.Range["E1:E1"].ColumnWidth = 10;
  63.                 worksheet.Cells[1, 6] = "Time";
  64.                 _pilotCount = f1Pilots.Count();
  65.                 int rowCount = 2;
  66.                 foreach (var f1Pilot in f1Pilots)
  67.                 {
  68.                     worksheet.Cells[rowCount, 2] = f1Pilot.Title;
  69.                     worksheet.Cells[rowCount, 3] = f1Pilot.CountryCode;
  70.                     worksheet.Cells[rowCount, 4] = f1Pilot.Constructor;
  71.                     worksheet.Cells[rowCount, 5] = f1Pilot.Engine;
  72.                     rowCount++;
  73.                 }
  74.                 worksheet.Range["A1:F1"].Select();
  75.                 ((Range)excelApp.Selection).Font.Bold = true;
  76.                 ((Range)excelApp.Selection).HorizontalAlignment = Microsoft.Office.Interop.Excel.XlHAlign.xlHAlignCenter;
  77.                 worksheet.Range[worksheet.Cells[2, 6], worksheet.Cells[rowCount – 1, 6]].Select();
  78.                 ((Range)excelApp.Selection).NumberFormat = "h:mm:ss";
  79.  
  80.             }
  81.             else
  82.             {
  83.                 MessageBox.Show("No active workbook!", "Warning",
  84.                 MessageBoxButtons.OK,
  85.                 MessageBoxIcon.Warning,
  86.                 MessageBoxDefaultButton.Button1);
  87.             }
  88.         }
  89.         catch (Exception ex)
  90.         {
  91.             _isConnected = false;
  92.             _ribbon.InvalidateControl("computeResultsButton");
  93.             MessageBox.Show(ex.Message, "Error",
  94.                 MessageBoxButtons.OK,
  95.                 MessageBoxIcon.Error,
  96.                 MessageBoxDefaultButton.Button1);
  97.         }
  98.     }
  99. }

The image below shows the populated worksheet. Note that the Compute Results button is now enabled.

image

After you type the time results or copy them from the results.txt file from the project folder to make your life a bit easier, you can click the Compute Results button that compute the order based on the time.

image

To tell the truth there is nothing extra in this action, since it is only populate the cells of the first column with an Excel formula. Of course we could do that in the same action we get the pilots in the first step, but it simply looks better this way in the demo.

  1. public void ComputeResultsButton_OnAction(IRibbonControl control)
  2. {
  3.     Microsoft.Office.Interop.Excel.Application excelApp = _addIn.Application;
  4.     Workbook workbook = excelApp.ActiveWorkbook;
  5.  
  6.     if (workbook != null)
  7.     {
  8.         Worksheet worksheet = (Worksheet)workbook.Sheets[1];
  9.         worksheet.Activate();
  10.         for (int rowCount = 2; rowCount <= _pilotCount + 1; rowCount++)
  11.         {
  12.             worksheet.Cells[rowCount, 1] = String.Format(@"= 1 + COUNTIF(F$2:F${0}, ""<"" & F{1})",
  13.                 _pilotCount, rowCount);
  14.         }
  15.     }
  16. }

After you have the results, you can switch to the backstage view where there is a new tab called Publish F1 Results. You can select the race you would like to publish results for. The data of the race, like location, date and image of the circuit if it is specified will be displayed. If the race is in the future, the Publish Results button is disabled.

When a race is selected from the dropdown list we set the selected race, then invalidate all of the controls that must be repainted to reflect the change:

  1. public void RaceSelected(IRibbonControl control, String itemId, int itemIndex)
  2. {
  3.     _currentRace = _f1Races.ElementAt(itemIndex);
  4.     _ribbon.InvalidateControl("raceLocation");
  5.     _ribbon.InvalidateControl("raceDate");
  6.     _ribbon.InvalidateControl("circuitImage");
  7.     _ribbon.InvalidateControl("noMapLabel");
  8.     _ribbon.InvalidateControl("publish");
  9. }

For example, to get the circuit image from the images folder of our SharePoint server using the WebClient class:

  1. public System.Drawing.Image GetCircuitImage(IRibbonControl control)
  2. {
  3.     if (GetImageVisible(control))
  4.     {
  5.         WebClient webClient = new WebClient();
  6.         webClient.Credentials = GetCredentials();
  7.         byte[] imageBytes = webClient.DownloadData(
  8.             String.Format(@"{0}/_layouts/images/VS2010Launch/circuits/{1}.png",
  9.             Properties.Settings.Default.SiteUrl,
  10.             _currentRace.CircuitImage));
  11.         MemoryStream imageStream = new MemoryStream(imageBytes);
  12.         System.Drawing.Image image = System.Drawing.Image.FromStream(imageStream);
  13.         System.Drawing.Image circuitImage = image.GetThumbnailImage((int)(image.Width * 0.7), (int)(image.Height * 0.7), null, System.IntPtr.Zero);
  14.         return circuitImage;
  15.     }
  16.     else
  17.     {
  18.         return null;
  19.     }
  20. }

The method is mapped again to the imageControl through the getImage attribute in the XML file:

  1. <imageControl id="circuitImage" getVisible="GetImageVisible" getImage="GetCircuitImage"/>

If you press the Publish Results button the results are published to the F1 Races list to the matching list item in the Result field as an XML text.

image

In the code we use again our data source created earlier to get the matching race item and update its Result field with the result XML:

  1. public void PublishResults_OnAction(IRibbonControl control)
  2. {
  3.     F1SiteDataContext dataContext = new F1SiteDataContext(
  4.                 new Uri(String.Format(@"{0}/_vti_bin/listdata.svc", Properties.Settings.Default.SiteUrl)));
  5.     dataContext.Credentials = GetCredentials();
  6.     var f1RaceCurrent = (from f1Race in dataContext.F1Races
  7.                             where f1Race.ID == _currentRace.ID
  8.                             select f1Race).First();
  9.     f1RaceCurrent.Result = GetResultXml();
  10.     dataContext.UpdateObject(f1RaceCurrent);
  11.     DataServiceResponse response = dataContext.SaveChanges();
  12.     MessageBox.Show("Result saved", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
  13.  
  14. }

And at this point we are back to SharePoint since updating the race item triggers our event receiver that generate the Word document. The F1RaceResultReceiver project item in the SharePoint solution contains the ResultDocCreator class that receives the result XML document and generates the Word document based on that. The code of this class is quite long and even more complicated, but I have to admit that I’ve not created that from scratch.

Note: To be able to run this code, you have to install the Open XML SDK 2.0 for Office on the SharePoint front-end.

First I’ve created a Word document that seems like the one I would like to generate. For this “prototype” I used the standard Word application UI.

Next I started the Open XML SDK 2.0 for Office Tool, opened my document and used the Reflect Code tab to get the code I need.

image

I used that code as a base version, but I had to work a bit more on that. Since it included separate code blocks with the same patterns for each items, I had to modify that to use iteration and loops for the same type of code. For example, original code included table row generation for all of the pilots. Instead of that I needed a single code block that generates a table row based on pilot data and call that code block for each of the pilots.

Probably one of the most interesting part of the code is the method that gets the binary content of the flag .png files from the images folder on SharePoint:

  1. private void GenerateImagePart1Content(ImagePart imagePart1, String nationality)
  2. {
  3.     String url = String.Format(@"{0}/_layouts/images/VS2010Launch/flags/{1}.png", _webUrl, nationality);
  4.     WebClient webClient = new WebClient();
  5.     webClient.UseDefaultCredentials = true;
  6.     byte[] imageBytes = webClient.DownloadData(url);
  7.     MemoryStream imageStream = new MemoryStream(imageBytes);
  8.     imagePart1.FeedData(imageStream);
  9.     imageStream.Close();
  10. }

When the Word document is generated its binary content is saved into the DocConverter library on SharePoint, as illustrated by the code of the event receiver:

  1. public override void ItemUpdated(SPItemEventProperties properties)
  2. {
  3.     SPListItem item = properties.ListItem;
  4.     SPWeb web = properties.Web;
  5.     String webUrl = properties.WebUrl;
  6.  
  7.     if (!(String.IsNullOrEmpty((String)item["Result"])))
  8.     {
  9.  
  10.         ResultDocCreator generator = new ResultDocCreator();
  11.  
  12.         String resultXmlString = (String)item["Result"];
  13.         MemoryStream stream = new MemoryStream();
  14.         generator.CreatePackage(stream, webUrl, resultXmlString);
  15.  
  16.         XmlDocument resultXml = new XmlDocument();
  17.         resultXml.LoadXml(resultXmlString);
  18.  
  19.         XmlNode raceNode = resultXml.SelectSingleNode("/Race");
  20.  
  21.         String docName = ResultDocCreator.GetStringAttribute(
  22.             raceNode, "title", null, true) + ".docx";
  23.  
  24.         SPList docConverter = web.Lists["DocConverter"];
  25.  
  26.         // just save the file, PDF conversion will be completed
  27.         // by the DocConverterReceiver event receiver
  28.         SPFile file = docConverter.RootFolder.Files.Add(
  29.             docName, stream, true);
  30.            
  31.     }
  32.     base.ItemUpdated(properties);
  33. }

Note: To be able to run the converter code, you have to create and configure a Word Automation Services service application on your SharePoint server. Since the conversation process runs asynchronously on the server I set the frequency to 1 minute to make the service more responsive on the demo.

image

I’ve created an event receiver bound to the DocConverter library. All this receiver does is to register a new PDF conversation job when a .docx document is saved into the library:

  1. public override void ItemAdded(SPItemEventProperties properties)
  2. {
  3.     SPListItem listItem = properties.ListItem;
  4.  
  5.     if ((listItem.Name.Length > 5) &&
  6.         (listItem.Name.Substring(listItem.Name.Length – 5).ToLower() == ".docx"))
  7.     {
  8.  
  9.         SPWeb web = properties.Web;
  10.         SPList list = properties.List;
  11.  
  12.         try
  13.         {
  14.  
  15.             EventFiringEnabled = false;
  16.  
  17.             // name of the configured service application for Word Automation Services
  18.             ConversionJob job = new ConversionJob("Word Automation Services");
  19.             job.UserToken = web.CurrentUser.UserToken;
  20.             job.Settings.UpdateFields = true;
  21.             job.Settings.OutputSaveBehavior = SaveBehavior.AlwaysOverwrite;
  22.             job.Settings.OutputFormat = SaveFormat.PDF;
  23.             String wordFile = String.Format("{0}/{1}", web.Url, listItem.Url);
  24.             String pdfFile = String.Format("{0}/{1}.pdf",
  25.                 web.Url, listItem.Url.Substring(0, listItem.Url.Length – 5));
  26.  
  27.             job.AddFile(wordFile, pdfFile);
  28.             job.Start();
  29.  
  30.  
  31.         }
  32.         finally
  33.         {
  34.             EventFiringEnabled = true;
  35.         }
  36.         base.ItemAdded(properties);
  37.     }
  38. }

You should wait a minute or two to let the conversation process complete.

At this point the DocConverter library should contain both the Word document and the PDF document for the race results.

image

Saving these files to your local machine and opening them you can see the results in Word:

image

Or with the very same content in Adobe Reader:

image

Thanks to Wikipedia authors for free circuit and flag images and to my F1-fun son for quality assurance all the data included in the demo applications.

Advertisements

3 Comments »

  1. […] de nem volt minden teljesen érthető a bemutatóból, azoknak ajánlom, hogy látogassanak el angol nyelvű blogomra, ahol a legérdekesebb kódrészleteket kiemelve is megtekinthetik, illetve akár az egész demó […]

    Pingback by Visual Studio 2010 és Team System 2010 termékbejelentő konferencia - Fejlesztés Office és SharePoint 2010-re - Holpár Péter szakmai blogja - devPortal — May 14, 2010 @ 01:54

  2. Nice demo application of office 2010 and sharepoint 2010 development.
    harePoint is a Microsoft Product, which is used to develop information portals in the organization.

    Comment by denisha — May 18, 2010 @ 06:56

  3. Great article! It does really make data access so easy from a variety of client apps. I’m going to use some of these concepts for a presentation I’ll be giving for the SharePoint 2010 launch.

    ps. And thanks for the reference to my REST blog post. 🙂

    Comment by Randy Williams — May 19, 2010 @ 14:58


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: