Thursday, October 11, 2012

Where to deploy frontend files? Style Library vs _Layouts folder

I'm not going to repeat the stuff you already familiar with. The goal of this post is to summarize some findings and make a personal conclusion based on experience without requirement to read a lot.

Where to deploy css and JavaScript files? Basically we have 2 options:

  • SharePoint layouts folder
  • "Style Library" document library

Style Library benefits:

  • Resources are limited to site collection
  • Smart caching (if it was deployed)
  • Ability to update it on couple of front-end servers (after that you can't deploy new version of file without resetting to site definition) through UI
  • Style Library in URL looks prettier than _LAYOTUS (at first look)

Style Library Problems:
  • Frontend guys are not really good in merging of .spdata files
  • Frontend guys are not really good in modifying path attributes in Elements.xml
  • Actually you need to be prepared to spend 10-50% of your time helping to Frontend guys
  • Deployments during development phase are painful
  • If file was updated manually on production you will forget to reset it
  • Lower performance
  • It's really annoying
  • Caching is not as smart as you might think. Especially with custom NLB.
Layouts folder benefits:
  • Easy to use, straightforward
  • No iisreset/application pool recycling required. You can modify files directly in 14 hive, then Ctrl+F5 and voila!
  • Deployment issues? Forget about them.
  • Merging issues? Forget it
  • Better performance
  • You can be sure that nobody modified your files manually. Sometimes it happens.

Layouts folder Problems:
  • Not scoped. You need just to organize your files
  • Caching. More on that later (few days hopefully)
  • Ugly URLs. More on that
  • Sandbox solutions. Probably it won't work there. But I haven't really worked with them because I don't see any value in Sandbox solutions at this point.
Conclusion: use Layouts folder for your frontend files and be happy with SharePoint. Don't make things more complicated than they are.

Interested links:
http://sharepoint.stackexchange.com/questions/2219/css-files-in-layouts-or-document-library?lq=1
http://www.sharepointnutsandbolts.com/2011/11/avoiding-bugs-from-cached-javascript.html
http://www.sharepointnutsandbolts.com/2009/05/optimization-blob-caching-and-http-304s.html

Saturday, November 12, 2011

Bug [SaveProduct doesn't work] and solution for it in "Pro ASP.NET MVC 3 Framework" by Steven Sanderson, Adam Freeman

During reading "Pro ASP.NET MVC 3 Framework" by Steven Sanderson, Adam Freeman I found that my code doesn't working (Chapter 9, page 267). There is the Repository method called SaveChanges and it doesn't work for me. The code is defined like below:

public void SaveProduct(Product product) {
            if (product.ProductID == 0) {
                context.Products.Add(product);
            }
            context.SaveChanges();
        }
In order to get it working you need to change it:
public void SaveProduct(Product product)
        {
            if (product.ProductID == 0)
            {
                m_Context.Products.Add(product);
            }
            else
            {
                m_Context.Entry(product).State = System.Data.EntityState.Modified;
            }

            m_Context.SaveChanges();
        } 
Hope this would help others in learning MVC using this incredible book!

Wednesday, November 9, 2011

SPFile.Item => "The object specified does not belong to a list"

Today I had an SPException getting SPFile.Item with message "The object specified does not belong to a list". I used the code like this:
var web = site.RootWeb;
var file = web.GetFile("http://site.url/test/pages/page.aspx");
if (file.Exists){
   var item = file.Item;
}
And then the exception was thrown. The fix was very simple:
using (var web = site.OpenWeb("/test/pages/page.aspx", false))
var file = web.GetFile("/test/pages/page.aspx");
...
}
I'm thinking that the original exception appeared because we created SPFile object from the SPWeb which doesn't contain this file. So, seems that SPFile should be explicitly created from corresponding SPWeb.
And one more strange issue:
using (var web = site.OpenWeb("/test/pages/page.aspx", false))
opens web with server relative URL "/test" while
using (var web = site.OpenWeb("http://site.url/test/pages/page.aspx", false))
returns RootWeb :O

Friday, November 4, 2011

Unit Testing Exercises

Found 2 interesting exercises which improves the understanding of TDD, Unit Testing and Type Mocking:
Kata 1
Kata 2

P.S. Kata (型 or 形 literally: "form"?) is a Japanese word describing detailed choreographed patterns of movements practised either solo or in pairs. The term form is used for the corresponding concept in non-Japanese martial arts in general. (from http://en.wikipedia.org/wiki/Kata)

SharePoint Batch Add, Update and Delete

Recently we needed to use SharePoint Batch commands to create, update and delete items in lists. I created a simple handler which allows to easily build such queries. And you're welcome to use it.

How to use it:
Assuming we have following DTO/Model object:

public class TestListItem : IContainsId
{
public int Id { get; set; }
public string SomeText { get; set; }
public int SomeCount { get; set; }
}
And we have a list with string column "CustomSomeText" and Number column "CustomSomeCount". We could use following code to batch add/update/delete this items to list:

var items = new List<TestListItem>
{
new TestListItem{Id = 1, SomeCount = 1, SomeText ="One"},
new TestListItem{Id = 3, SomeCount = 9, SomeText ="Three"},
new TestListItem{Id = 5, SomeCount = 25, SomeText ="Twenty Five"}
};
var builder = new BatchBuilder<TestListItem>();
var fields = new[] { "CustomSomeText", "CustomSomeCount" };
var selectors = new Func<TestListItem, string>[] { x => x.SomeText, x => x.SomeCount.ToString() };
using (var site = new SPSite("http://sitecollection"))
{
var web = site.RootWeb;
var list = web.Lists["batch"];
//var command = builder.GetAddCommand(items, fields, selectors, list.ID);
//var command = builder.GetUpdateCommand(items, fields, selectors, list.ID);
var command = builder.GetDeleteCommand(items, list.ID);
string result = web.ProcessBatchData(command);
Console.WriteLine(result);
}
I think it's pretty straightforward.



IContainsId.cs

public interface IContainsId
{
    int Id { get; }
}

BatchBuilder.cs

public class BatchBuilder<T> where T : IContainsId
{
    #region Constants
    public const string BATCH_START =
        "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ows:Batch OnError=\"Return\">";
    public const string BATCH_END = "</ows:Batch>";
    public const string UPDATE_COMMAND_FORMAT = "<Method ID=\"{0}\">" +
                                "<SetList>{1}</SetList>" +
                                "<SetVar Name=\"Cmd\">Save</SetVar>" +
                                "<SetVar Name=\"ID\">{2}</SetVar>{3}" +
                                "</Method>";
    public const string ADD_COMMAND_FORMAT = "<Method ID=\"{0}\">" +
                                "<SetList>{1}</SetList>" +
                                "<SetVar Name=\"Cmd\">Save</SetVar>" +
                                "<SetVar Name=\"ID\">New</SetVar>{2}" +
                                "</Method>";
    public const string DELETE_COMMAND_FORMAT = "<Method ID=\"{0}\">" +
                                "<SetList>{1}</SetList>" +
                                "<SetVar Name=\"Cmd\">Delete</SetVar>" +
                                "<SetVar Name=\"ID\">{0}</SetVar>" +
                                "</Method>";
    #endregion
    #region Public Methods
    public string GetUpdateCommand(IEnumerable<T> itemsToUpdate, string[] fieldNames, Func<T, string>[] selectors, Guid listId)
    {
        if (fieldNames.Length == 0 || selectors.Length == 0 || fieldNames.Length != selectors.Length)
        {
            throw new ArgumentException();
        }
        var batchBuilder = new StringBuilder(BATCH_START);
        foreach (var itemToUpdate in itemsToUpdate)
        {
            var id = itemToUpdate.Id;
            var fieldsBuilder =
                fieldNames.Select(
                    (x, i) => string.Format("<SetVar Name=\"urn:schemas-microsoft-com:office:office#{0}\">{1}</SetVar>", x, selectors[i].Invoke(itemToUpdate)))
                    .Aggregate(new StringBuilder(), (c, n) => c.Append(n));
            batchBuilder.AppendFormat(UPDATE_COMMAND_FORMAT, id, listId, id, fieldsBuilder);
        }
        batchBuilder.Append(BATCH_END);
        return batchBuilder.ToString();
    }
    public string GetAddCommand(IEnumerable<T> itemsToAdd, string[] fieldNames, Func<T, string>[] selectors, Guid listId)
    {
        if (fieldNames.Length == 0 || selectors.Length == 0 || fieldNames.Length != selectors.Length)
        {
            throw new ArgumentException();
        }
        var batchBuilder = new StringBuilder(BATCH_START);
        var methodId = 1;
        foreach (var itemToAdd in itemsToAdd)
        {
            var fieldsBuilder =
                fieldNames.Select(
                    (x, i) => string.Format("<SetVar Name=\"urn:schemas-microsoft-com:office:office#{0}\">{1}</SetVar>", x, selectors[i].Invoke(itemToAdd)))
                    .Aggregate(new StringBuilder(), (c, n) => c.Append(n));
            batchBuilder.AppendFormat(ADD_COMMAND_FORMAT, methodId, listId, fieldsBuilder);
            ++methodId;
        }
        batchBuilder.Append(BATCH_END);
        return batchBuilder.ToString();
    }
    public string GetDeleteCommand(IEnumerable<T> itemsToDelete, Guid listId)
    {
        var batchBuilder = new StringBuilder(BATCH_START);
        foreach (var itemToDelete in itemsToDelete)
        {
            batchBuilder.AppendFormat(DELETE_COMMAND_FORMAT, itemToDelete.Id, listId);
        }
        batchBuilder.Append(BATCH_END);
        return batchBuilder.ToString();
    }
    #endregion
}


List Event Receiver and Feature Scope

Recently my colleague created an event receiver which was going to be instantiated for all picture libraries at the site collection. He created a receiver and bound it like this:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Receivers ListTemplateId="109">
      <Receiver>
        <Name>PictureLibraryEventReceiverItemUpdating</Name>
        <Type>ItemUpdating</Type>
        <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
        <Class>Company.Features.PictureLibraryEventReceiver</Class>
        <SequenceNumber>10008</SequenceNumber>
      </Receiver>
      <Receiver>
        <Name>PictureLibraryEventReceiverItemDeleting</Name>
        <Type>ItemDeleting</Type>
        <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
        <Class>Company.Features.PictureLibraryEventReceiver</Class>
        <SequenceNumber>10009</SequenceNumber>
      </Receiver>
  </Receivers>
  <Receivers ListTemplateId="851">
    <Receiver>
      <Name>PictureLibraryEventReceiverItemUpdating</Name>
      <Type>ItemUpdating</Type>
      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
      <Class>Company.Features.PictureLibraryEventReceiver</Class>
      <SequenceNumber>10010</SequenceNumber>
    </Receiver>
    <Receiver>
      <Name>PictureLibraryEventReceiverItemDeleting</Name>
      <Type>ItemDeleting</Type>
      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
      <Class>Company.Features.PictureLibraryEventReceiver</Class>
      <SequenceNumber>10011</SequenceNumber>
    </Receiver>
  </Receivers>
</Elements>
Unfortunately, it turned out that SharePoint (2010) added this receivers to all lists and libraries ignoring ListTemplateId parameter. I started investigating what's wrong and expected that my colleague made a mistake.

I noticed that feature scope is Site which is kind of logical since we're going to use this receiver in subsites as well. So, here starts an interesting part. I found out that MOSS 2010 uses following behavior:


  1. If feature scope is site
    1. If receiver scope is set to site = provision receiver to all lists of site collection (list template/url is not taken into consideration)!
    2. If receiver scope is set to web = provision receiver to all lists of the particular web (list template/url is not taken into consideration)!
  2. If feature scope is web
    1. If list template and list url are not set = 1.1
    2. List template = provision to all lists of correct type
    3. List url = provision receiver to the particular list




Monday, July 4, 2011

Open the tool pane link from web part in MOSS 2010

In our project our web parts which were not configured yet render simple description and link to open web part properties in following format:
string.Format(@"<a href=\"#\" onclick=\"javascript:MSOTlPn_ShowToolPane2('Edit','{0}');\">open the tool pane</a>", webPart.ID)
It worked fine for MOSS 2007 but in MOSS 2010 when page is not in edit mode such link threw a javascript error.

Thanks to http://kvdlinden.blogspot.com/2010/04/open-tool-pane-javascript-error-in.html issue has been fixed. All you need is to add following control to the master page:
<SharePoint:ScriptLink language="javascript" name="ie55up.js" OnDemand="false" runat="server" />