Monday, May 23, 2011

SPPostIt - It's Almost Here!

So I have been working quite a bit on SPPostIt and there is a lot to show you in this post. If you saw the last post, you might wonder what I was thinking with my design of the form that I was using. My better half pointed out that though it was nice, it just was too much, and I reluctantly had to agree!

So, I revamped the idea and came up with what I think is a better option. A floating bar that you can drag to wherever you need. But there is a lot built into it! So let me show you a few shots of what it looks like after it is loaded.


So I think that this is much better and is really clean. The bar can be dragged from the postit icon on the left. The arrow icon will dropdown the tabbed view and notice that the icon changes to an up icon. It's the little things right?! You will also notice what it has in the bar. "You have 1 unread message." The messages are checked based on a timer and could be changed. Currently it is 15 seconds.

So the next views will give you more looks at what is coming.


 Click on a message header and it slides down to reveal the message body. The check mark will allow you to mark it as "read".


The view below is how to send a message. The people picker was fun to reproduce, but it works great! The rich text editor works well but is only supported in IE. I am looking into options for other browsers.


Clicking the send button yields the below notice.


Notice now that there are 3 unread messages. I have already expanded it out.


So I hope this gives you  a great look into SPPostIt and I hope to hear feedback from you!

Stay tuned for more!

Friday, May 20, 2011

SPPostIt - The Beginning

A short time ago, I asked a few folks their opinion on an idea that I discussed in this post. I received very welcomed feedback and set to work on what I hope will be a great tool that people will want to use. As you may guess by the post's title, I have currently opted to call my tool SPPostIt. It will start as a fairly simple messaging tool that would be used in SharePoint to send messages to other users on a site. The cool part is that all the functionality is held in a jQuery plugin. So the two requirements so far is that you have jQuery and SharePoint.

I will tell you that this is being created for many reasons but the main drivers for this currently are:
  • No HTML email option. All emails look icky .(technical term!)
  • Users have multiple email accounts for security reasons.
There are other reasons as well, but these 2 are probably the biggest. Once I got the idea however, I saw a lot of potential for future growth and use. I will discuss more of that later. I wanted to go over what I have created so far and let you get a peek at some screen shots of what it looks like as of now.

Currently in my environment, I have to use Visual Studio 2008 to create solutions and I also have to use VSEWSS. I unlike most, however, actually like this set as it was just easy for me to pick it up and use. So what I did was create an empty SharePoint solution and went from there. As I am putting the functions in a jQuery plugin, you would not be required to deploy a solution per se, but there is a css file, and some images. My solution also deploys two lists. One to hold the messages and one to hold message categories. Fairly simple solution and I hope to have it up on Codeplex soon.

Enough Talk Let's Have Some Screenshots!

When I activate the feature the jquery plugin is loaded via an ascx control into the masterpage. This is not the only way to do it, just how I did it. It adds an option to ECB menus and  to all the "Actions" menus for list/library views and looks like this:


After clicking this, it will open a popup dialog that looks like this:


Of course, you could change this, but why would you want to ;-)

This is just the beginning and I hope it is well received. Please feel free to provide feedback of any kind!

Friday, May 6, 2011

Sharepoint Messaging System Concept Phase

A SharePoint Messaging System

THE CONCEPT

     So what exactly would a SharePoint Messaging System be about? What do I mean and why do I thnk it is a good idea? I wanted to use this post to go over the concept and then go into more detail in subsequent posts. But first, I would like to present some background that led me down this path.

     Currently I work in an environment with WSS 3 only. We are possibly going to upgrade to SP 2007 or may skip that one and go straight to SP 2010. There is no time line for that either. I am asked to create Sharepoint solutions that require a lot of work and I normally do most development in Visual Studio 2008. I do not have SPD 2007 on my production machine though I do have it on my development machine. So I have some tools to develop with. I am allowed to write .Net code or any code needed to get the job done so there really is no limit in that regard. So that is that side.

     The next piece I wanted to to share is that our email is plain text only email and any html is stripped out. So unfortunately alerts just look rather ugly even if they are customized to look somewhat better. So there is the meat of our environment. Not the best, but at least we have SharePoint!

     So with that said, I have been working on several sites and I really try to make things easy whenever I can. I am working on 2 major projects right now and both clients want to have something better than just email alerts that look ugly or emails that do not give them much detail. I was showing each of them some stuff with a custom Edit Control Block and the things that I could do with it. They were excited about the possibilities and I was asked if I could have an option to send a message to the "Assigned To" user or the "Author" of an item from the ECB. "Of Course I Can!" was my reply.

     It was at that point as I had already written several event receivers and a timer job that send emails that I just felt I needed to have something extra. A light bulb turned on and I have not been able to turn it off yet! I thought that yes, I could email users for sure, but what if I could also use SharePoint and a site to build a real messaging center. Something that could tell a user who was on any page in SharePoint that they had something new to look at and they could see it with full HTML without it being ugly. That was when the thought stuck and that is why I am writing this.

     There is an endless amount of ways I could do this thought I have a few in mind that I am going to lay out in more detail soon. But basicaly here is a pseudo list of how this might work.

  • User decides he needs to message a coworker for whatever reason.
  • User clicks a link on a page or item in an ECB to message someone.
  • A popup (jQuery UI dialog maybe) opens to present the messaging form.
  • User fills it out and hits OK/SEND or whatever.
  • If the recipient is on a Sharepoint site they will get the notification.
  • Recipient opens the message.
     I know this is just a small part of what you could really do, but I think that this has a lot of possibilities. I think that I want to do this using jQuery and SPServices for most of the work. I could wrap it in a feature or something like that and it could be added to any site that wants to support it.

     Well, there it is in a proverbial nutshell. Is it a good idea, mediocre at best or what? Any thoughts are appreciated!

Thursday, May 5, 2011

Creating A Custom Edit Control Block Part 2

     So in the last post, I went over the basic means for getting a custom ECB up and running. In this post, I will give more detailed examples and how to use .Net code to really give your ECB some nice features! So moving on to a more detailed example. The following image shows a custom ECB created using javascript and .Net object model code:


As you can see, there is a lot of functionality in there. It is security trimmed by the item and determines what the current user can do. The Repeat options allow the author of the item to basically copy the commitment to another department/division/ branch in this case. So how do we do this you ask? Let's take a look at some code that makes this kind of thing happen. First the javascript code:

function Custom_AddListMenuItems(m, ctx) {
    if (test.indexOf("Lists/Commitments") > 0) {
 var request;
 var url = ctx.HttpRoot + "/CustomPages/ECB.aspx?ListID=" + ctx.listName + "&ItemID=" + currentItemID + "&DateTime=" + Date();
 request = new ActiveXObject("Microsoft.XMLHTTP");
      if (request) {
  request.open("GET", url, false);
  request.send();
      }
      if (request) {
  var commands = request.responseXML.getElementsByTagName("Command");
  var dvs, brs;
  var menuitema, menuitemb, menuitemc, menuitemd, menuiteme;
  var menuitemsd = new Array();
  var menuitemse = new Array();
  var menuitemsf = new Array();
  var menuitems = new Array(100, 100);
  var chka = 0;
  var chkb = 0;
  var chkc = 0;
  var chkd = 0;
  var chke = 0;
  var tp0, tp1, tp2, tp3, tp4, tp5;
  var ddbr;
   CAMOpt(m, "Print Item...", "_nnsy_printListItem()", "/_layouts/images/fax.gif");
   // Add separator
   CAMSep(m);
   for (var i = 0; i < commands.length; i++) {
       var cmdName = commands[i].getElementsByTagName("Name")[0].firstChild.nodeValue;
       var imageUrl = commands[i].getElementsByTagName("ImageUrl")[0].firstChild.nodeValue;
       var type = commands[i].getElementsByTagName("Type")[0].firstChild.nodeValue;
       switch (type) {
           case "Seperator":
               CAMSep(m);
               break;

           case "MenuItem":
               var js = commands[i].getElementsByTagName("Script")[0].firstChild.nodeValue;
               CAMOpt(m, cmdName, js, imageUrl);
               break;

           case "Concurrence":
               var js = commands[i].getElementsByTagName("Script")[0].firstChild.nodeValue;
               if (chkb == 0) {
                   menuitemb = CASubM(m, "Concurrences", "", "/_layouts/images/forward.gif");
                   menuitemsd[chkb] = CASubM(menuitemb, cmdName, imageUrl)
                   CAMOpt(menuitemsd[chkb], "View", "viewconcurrence('" + js + "', 'Concurrence', 'View')", imageUrl);
                   CAMOpt(menuitemsd[chkb], "Edit", "editconcurrence('" + js + "', 'Concurrence', 'Edit')", imageUrl);
                   CAMOpt(menuitemsd[chkb], "Concur", "editconcurrence('" + js + "', 'Concurrence', 'Concur')", imageUrl);
                   chkb += 1;
               }
               else {
                   menuitemsd[chkb] = CASubM(menuitemb, cmdName, imageUrl)
                   CAMOpt(menuitemsd[chkb], "View", "viewconcurrence('" + js + "', 'Concurrence', 'View')", imageUrl);
                   CAMOpt(menuitemsd[chkb], "Edit", "editconcurrence('" + js + "', 'Concurrence', 'Edit')", imageUrl);
                   CAMOpt(menuitemsd[chkb], "Concur", "editconcurrence('" + js + "', 'Concurrence', 'Concur')", imageUrl);
                   chkb += 1;
               }
               break;

           case "Extension":
               var js = commands[i].getElementsByTagName("Script")[0].firstChild.nodeValue;
               if (chkc == 0) {
                   menuitemc = CASubM(m, "Extensions", "", "/_layouts/images/forward.gif");
                   menuitemse[chkc] = CASubM(menuitemc, cmdName, imageUrl)
                   CAMOpt(menuitemse[chkc], "View", "viewconcurrence('" + js + "', 'Extension', 'View')", imageUrl);
                   CAMOpt(menuitemse[chkc], "Edit", "editconcurrence('" + js + "', 'Extension', 'Edit')", imageUrl);
                   CAMOpt(menuitemse[chkc], "Concur", "editconcurrence('" + js + "', 'Extension', 'Concur')", imageUrl);
                   chkc += 1;
               }
               else {
                   menuitemse[chkc] = CASubM(menuitemc, cmdName, imageUrl)
                   CAMOpt(menuitemse[chkc], "View", "viewconcurrence('" + js + "', 'Extension', 'View')", imageUrl);
                   CAMOpt(menuitemse[chkc], "Edit", "editconcurrence('" + js + "', 'Extension', 'Edit')", imageUrl);
                   CAMOpt(menuitemse[chkc], "Concur", "editconcurrence('" + js + "', 'Extension', 'Concur')", imageUrl);
                   chkc += 1;
               }
               break;

           case "Closeout":
               var js = commands[i].getElementsByTagName("Script")[0].firstChild.nodeValue;
               if (chkd == 0) {
                   menuitemd = CASubM(m, "Closeout", "", "/_layouts/images/forward.gif");
                   CAMOpt(menuitemd, "View", "viewconcurrence('" + js + "', 'Closeout', 'View')", imageUrl);
                   CAMOpt(menuitemd, "Edit", "editconcurrence('" + js + "', 'Closeout', 'Edit')", imageUrl);
                   CAMOpt(menuitemd, "Concur", "editconcurrence('" + js + "', 'Closeout', 'Concur')", imageUrl);
                   chkd += 1;
               }
               else {
                   CAMOpt(menuitemd, "View", "viewconcurrence('" + js + "', 'Closeout', 'View')", imageUrl);
                   CAMOpt(menuitemd, "Edit", "editconcurrence('" + js + "', 'Closeout', 'Edit')", imageUrl);
                   CAMOpt(menuitemd, "Concur", "editconcurrence('" + js + "', 'Closeout', 'Concur')", imageUrl);
                   chkd += 1;
               }
               break;


           case "CopyToDept":
               var xmldata1 = new ActiveXObject("MSXML.DomDocument");
               xmldata1.loadXML(request.responseText);
               var depts = xmldata1.selectNodes("ECB/Departments/Department");
               menuitems[99, 99] = CASubM(m, "Repeat To Department");
               for (var j = 0; j < depts.length; j++) {
                   tp0 = depts[j].childNodes[0].firstChild.nodeValue;
                   CAMOpt(menuitems[99, 99], tp0, "copyitem('" + cmdName + "', '" + tp0 + "', '" + null + "', '" + null + "')", "/_layouts/images/forward.gif");
               }
               break;

           case "CopyToDiv":
               var xmldata1 = new ActiveXObject("MSXML.DomDocument");
               xmldata1.loadXML(request.responseText);
               var depts = xmldata1.selectNodes("ECB/Departments/Department");
               menuitems[99, 99] = CASubM(m, "Repeat To Division");
               for (var j = 0; j < depts.length; j++) {
                   tp0 = depts[j].childNodes[0].firstChild.nodeValue;
                   menuitems[j, 0] = CASubM(menuitems[99, 99], tp0);
                   dvs = depts[j].childNodes[1].childNodes;
                   for (var k = 0; k < dvs.length; k++) {
                       tp1 = dvs[k].childNodes[0].firstChild.nodeValue;
                       CAMOpt(menuitems[j, 0], tp1, "copyitem('" + cmdName + "', '" + tp0 + "', '" + tp1 + "', '" + null + "')", "/_layouts/images/forward.gif");
                   }
               }
               break;


           case "CopyToBranch":
               var xmldata1 = new ActiveXObject("MSXML.DomDocument");
               xmldata1.loadXML(request.responseText);
               var depts = xmldata1.selectNodes("ECB/Departments/Department");
               menuitems[99, 99] = CASubM(m, "Repeat To Branch");
               for (var j = 0; j < depts.length; j++) {
                   tp0 = depts[j].childNodes[0].firstChild.nodeValue;
                   menuitems[j, 0] = CASubM(menuitems[99, 99], tp0);
                   dvs = depts[j].childNodes[1].childNodes;
                   for (var k = 0; k < dvs.length; k++) {
                       tp1 = dvs[k].childNodes[0].firstChild.nodeValue;
                       menuitems[j, k + 1] = CASubM(menuitems[j, 0], tp1);
                       brs = dvs[k].childNodes[1].childNodes;
                       for (var n = 0; n < brs.length; n++) {
                           tp2 = brs[n].childNodes[0].firstChild.nodeValue;
                           CAMOpt(menuitems[j, k + 1], tp2, "copyitem('" + cmdName + "', '" + tp0 + "', '" + tp1 + "', '" + tp2 + "')", "/_layouts/images/forward.gif");
                       }
                   }
               }
               break;
       }

   }
   if (commands.length > 0) CAMSep(m);
   return true;
   //return false; // for testing
  }
 }
 else {
  return false;
 }  
}

So as you can see here there is a lot going on. The first step is to setup an ajax request to the .aspx page that has the object model code. This could be done using inline script or code behind which is the better choice. This page is actually in a document library that users would have read access to. So the request object sends requests to the page and sends the list and item id's in the querystring. This page has several responsibilities. It must return valid xml to be used by the javascript code. It will also determine the users ability based on what the requirements are. In the code below, there is a lot of stuff to go through but I will try to tie it all together in the end.

private void Page_Load(Object sender, EventArgs e)
{
    SPWeb web = SPContext.Current.Web;
    SPUser curuser;
    String author = String.Empty;
    String cuser = String.Empty;
    SPListItem item = null;
    String qry;
    int itemID = 1;
    curuser = SPContext.Current.Web.CurrentUser;
    SPList emps = web.Lists["User Information List"];
    SPList status = web.Lists["Status"];
    SPList ddbr = web.Lists["DDBR"];
    SPList prefs = web.Lists["Preferences"];
    SPListItem pref = prefs.Items.GetItemById(1);
    SPListItem user = emps.Items.GetItemById(Convert.ToInt32(curuser.ID));
    SPList list = web.Lists["Commitments"];
    SPList concs = web.Lists["Concurrences"];
    Boolean goon = true;
    try
    {
        itemID = int.Parse(this.Page.Request.QueryString["ItemID"]);
        item = list.Items.GetItemById(itemID);
        author = item["Author"].ToString();
        int loc = author.IndexOf("#");
        author = author.Remove(0, loc + 1);
        cuser = curuser.Name.ToString();
    }
    catch (Exception ex){ author = ex.Message.ToString() + ", " + ex.Source.ToString(); }
        
    String xhtml = "";

    String asgdiv = String.Empty;
    if (item["AssignedDivision"] != null) { asgdiv = item["AssignedDivision"].ToString(); }
    String asgdept;
    asgdept = item["AssignedDepartment"].ToString();
        
    this.Page.Response.ClearHeaders();
    this.Page.Response.ClearContent();
    this.Page.Response.Cache.SetCacheability(HttpCacheability.NoCache);
    this.Page.Response.AddHeader("Content-type", "text/xml");

    string cmdPatterna = @"";
    
    xhtml += "";
    xhtml += "";    
    String udept = "";
    String udiv = "";
    String ubr = "";
    SPQuery dpqry;
    SPQuery dvqry;

    xhtml += "";
    foreach (SPListItem x in ddbr.Items)
    {
        if (x["Department"].ToString() != udept)
        {
            udept = x["Department"].ToString();
            xhtml += string.Format("{0}", udept);
            dpqry = new SPQuery();
            dpqry.Query = "" + udept + "";
            foreach (SPListItem y in ddbr.GetItems(dpqry))
            {
                if (y["Division"].ToString() != udiv)
                {
                    udiv = y["Division"].ToString();
                    xhtml += string.Format("{0}", udiv);
                    dvqry = new SPQuery();
                    dvqry.Query = "" + udiv + "";
                    foreach (SPListItem z in ddbr.GetItems(dvqry))
                    {
                        if (z["Branch"].ToString() != ubr)
                        {
                            ubr = z["Branch"].ToString();
                            xhtml += string.Format("{0}", ubr);
                        }
                    }
                    xhtml += "";
                    xhtml += "";
                }
            }
            xhtml += "";
            xhtml += "";
        }
    }
    xhtml += "";
    xhtml += "";

    if (list.DoesUserHavePermissions(SPBasePermissions.CreateAlerts)) { xhtml += string.Format(cmdPatterna, "MenuItem", "Alert Me", Page.ResolveUrl("~/_layouts/images/outl.gif"), "alertme('" + itemID + "', '{" + list.ID.ToString() + "}')"); }
    if (list.DoesUserHavePermissions(SPBasePermissions.ViewListItems)) { xhtml += string.Format(cmdPatterna, "MenuItem", "View Commitment", Page.ResolveUrl("~/_layouts/images/openfold.gif"), "viewitem('" + itemID + "', '" + item["Priority"].ToString() + "')"); }
    if (list.DoesUserHavePermissions(SPBasePermissions.ViewVersions)) { xhtml += string.Format(cmdPatterna, "MenuItem", "Version History", Page.ResolveUrl("~/_layouts/images/versions.gif"), "versions('" + itemID + "', '{" + list.ID.ToString() + "}')"); }
    xhtml += string.Format(cmdPatterna, "Seperator", null, null, null);
        
    try
    {
        if (web.IsCurrentUserMemberOfGroup(web.Groups["CTS Content Manager"].ID) == true || web.IsCurrentUserMemberOfGroup(web.Groups["WebAdmin"].ID) == true || web.IsCurrentUserMemberOfGroup(web.Groups["CTS Owners"].ID) == true || author == curuser.Name.ToString()) //  || author == curuser.Name.ToString()
        {
            xhtml += string.Format(cmdPatterna, "CopyToDept", itemID, null, null);
            xhtml += string.Format(cmdPatterna, "CopyToDiv", itemID, null, null);
            xhtml += string.Format(cmdPatterna, "CopyToBranch", itemID, null, null);
            xhtml += string.Format(cmdPatterna, "Seperator", null, null, null);
            xhtml += string.Format(cmdPatterna, "MenuItem", "Create Concurrence Request", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "concurrence('" + itemID + "')");
            xhtml += string.Format(cmdPatterna, "MenuItem", "Create Complete Request", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "createcloseout('" + itemID + "')");
            xhtml += string.Format(cmdPatterna, "MenuItem", "Revise ECD", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "revecd('" + itemID + "')");
            xhtml += string.Format(cmdPatterna, "Seperator", null, null, null);
            xhtml += string.Format(cmdPatterna, "MenuItem", "Edit Commitment", Page.ResolveUrl("~/_layouts/images/edit.gif"), "edititem('" + itemID + "', '" + item["Priority"].ToString() + "')");
            xhtml += string.Format(cmdPatterna, "MenuItem", "Complete Commitment", Page.ResolveUrl("~/_layouts/images/star.gif"), "closeitem('" + itemID + "')"); 
            if (list.DoesUserHavePermissions(SPBasePermissions.DeleteListItems)) { xhtml += string.Format(cmdPatterna, "MenuItem", "Delete Commitment", Page.ResolveUrl("~/_layouts/images/delete.gif"), "deleteitem('" + itemID + "')"); }
            if (list.DoesUserHavePermissions(SPBasePermissions.ManagePermissions)) { xhtml += string.Format(cmdPatterna, "MenuItem", "Manage Permissions", Page.ResolveUrl("~/_layouts/images/managePerm.gif"), "permissions('" + itemID + "', '{" + list.ID.ToString() + "}')"); }
        }
        else
        {
            if (web.IsCurrentUserMemberOfGroup(web.Groups["CTS Power Users"].ID) == true || web.IsCurrentUserMemberOfGroup(web.Groups["CTS Users"].ID) == true)
            {
                if (item["Priority"].ToString().Contains("Interdepartmental"))  // Interdepartmental commitment
                {
                    if (item["AssignedDepartment"].ToString().Equals(user["CTSDepartment"].ToString()))
                    {
                        xhtml += string.Format(cmdPatterna, "MenuItem", "Create Concurrence Request", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "concurrence('" + itemID + "')");
                        xhtml += string.Format(cmdPatterna, "MenuItem", "Revise ECD", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "revecd('" + itemID + "')"); 
                        xhtml += string.Format(cmdPatterna, "MenuItem", "Edit Commitment", Page.ResolveUrl("~/_layouts/images/edit.gif"), "edititem('" + itemID + "', '" + item["Priority"].ToString() + "')");
                        if (list.DoesUserHavePermissions(SPBasePermissions.DeleteListItems))
                        {
                            xhtml += string.Format(cmdPatterna, "MenuItem", "Complete Commitment", Page.ResolveUrl("~/_layouts/images/star.gif"), "closeitem('" + itemID + "')");
                            xhtml += string.Format(cmdPatterna, "MenuItem", "Delete Commitment", Page.ResolveUrl("~/_layouts/images/delete.gif"), "deleteitem('" + itemID + "')");
                        }
                        else
                        {
                            if (pref["AllowCloseout"].ToString().Equals("Yes"))
                            {
                                if (item["Status"].ToString().Equals("In Progress")) { xhtml += string.Format(cmdPatterna, "MenuItem", "Complete Commitment", Page.ResolveUrl("~/_layouts/images/star.gif"), "closeitem('" + itemID + "')"); }
                            }
                            else
                            {
                                if (item["Status"].ToString().Equals("In Progress")) { xhtml += string.Format(cmdPatterna, "MenuItem", "Create Complete Request", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "createcloseout('" + itemID + "')"); }
                            }
                        }
                    }
                }
                else  // item is critical or commitment
                {
                    xhtml += string.Format(cmdPatterna, "MenuItem", "Create Concurrence Request", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "concurrence('" + itemID + "')");
                    xhtml += string.Format(cmdPatterna, "MenuItem", "Revise ECD", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "revecd('" + itemID + "')");
                    xhtml += string.Format(cmdPatterna, "MenuItem", "Edit Commitment", Page.ResolveUrl("~/_layouts/images/edit.gif"), "edititem('" + itemID + "', '" + item["Priority"].ToString() + "')");
                    if (list.DoesUserHavePermissions(SPBasePermissions.DeleteListItems))
                    {
                        xhtml += string.Format(cmdPatterna, "MenuItem", "Complete Commitment", Page.ResolveUrl("~/_layouts/images/star.gif"), "closeitem('" + itemID + "')");
                        xhtml += string.Format(cmdPatterna, "MenuItem", "Delete Commitment", Page.ResolveUrl("~/_layouts/images/delete.gif"), "deleteitem('" + itemID + "')");
                    }
                    else
                    {
                        if (pref["AllowCloseout"].ToString().Equals("Yes"))
                        {
                            if (item["Status"].ToString().Equals("In Progress")) { xhtml += string.Format(cmdPatterna, "MenuItem", "Complete Commitment", Page.ResolveUrl("~/_layouts/images/star.gif"), "closeitem('" + itemID + "')"); }
                        }
                        else
                        {
                            if (item["Status"].ToString().Equals("In Progress")) { xhtml += string.Format(cmdPatterna, "MenuItem", "Create Complete Request", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "createcloseout('" + itemID + "')"); }
                        }
                    }
                }
            }
            else
            {
                if (web.IsCurrentUserMemberOfGroup(web.Groups["CTS Dept Power Users"].ID) == true || web.IsCurrentUserMemberOfGroup(web.Groups["CTS Dept Users"].ID) == true)
                {
                    if (item["Priority"].ToString().Contains("Interdepartmental"))  // Interdepartmental commitment
                    {
                        if (item["AssignedDepartment"].ToString().Equals(user["CTSDepartment"].ToString()))
                        {
                            xhtml += string.Format(cmdPatterna, "MenuItem", "Create Concurrence Request", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "concurrence('" + itemID + "')");
                            xhtml += string.Format(cmdPatterna, "MenuItem", "Revise ECD", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "revecd('" + itemID + "')");
                            xhtml += string.Format(cmdPatterna, "MenuItem", "Edit Commitment", Page.ResolveUrl("~/_layouts/images/edit.gif"), "edititem('" + itemID + "', '" + item["Priority"].ToString() + "')");
                            if (pref["AllowCloseout"].ToString().Equals("Yes"))
                            {
                                if (item["Status"].ToString().Equals("In Progress")) { xhtml += string.Format(cmdPatterna, "MenuItem", "Complete Commitment", Page.ResolveUrl("~/_layouts/images/star.gif"), "closeitem('" + itemID + "')"); }
                            }
                            else
                            {
                                if (item["Status"].ToString().Equals("In Progress")) { xhtml += string.Format(cmdPatterna, "MenuItem", "Create Complete Request", Page.ResolveUrl("~/_layouts/images/icspgen.gif"), "createcloseout('" + itemID + "')"); }
                            }
                            if (list.DoesUserHavePermissions(SPBasePermissions.DeleteListItems) && item["Status"].ToString().Equals("In Progress")) { xhtml += string.Format(cmdPatterna, "MenuItem", "Delete Commitment", Page.ResolveUrl("~/_layouts/images/delete.gif"), "deleteitem('" + itemID + "')"); }
                        }
                        else
                        {
                            // not in assigned dept!
                        }
                    }
                    else  //critical or commitment
                    {
                        // Already have the print and view options at the top
                    }
                }
            }
        }            
    }
        
    catch { }
        
    xhtml += "";
    this.Page.Response.Write(xhtml);
    this.Page.Response.End();          
}

There is one thing I want to point out here in that due to the code formatting, the commandpattern line is not formatted correctly due to the </Script> tag. If you pasted this code into Visual Studio for example, it will show an error unless you escape the slash with a \

Now, as you can see, there is a lot of stuff here. I wanted to get it out there for you and I will go over it more in the next post. Sorry, but there is a lot here and I really want to get into it! Until then!

Tuesday, May 3, 2011

Creating A Custom Edit Control Block Part 1



What is an Edit Control Block?
(And why would I want a custom one?)

The image below is an example of an Edit Control Block or ECB as it is normally called:

This is the ECB of a tasks list. There are many things that can be said based on just this. The user has edit and delete permissions on this item, and can even manage the items permissions. You will find that out of the box, the ECB is different depending on the type of list or library you are viewing and the permissions of the user. The cool thing here though, is that you can completly customize this depending on your needs. There are a few methods that you can use to do this and I will outline them briefly.


  1. javascript/jQuery - This is a must! You must write javascript code (ack the "c" word!). The functions that allow you to do this are not too difficult and we will go through them next. You can build a great custom ECB with just this method.
  2. .Net (c#) code. (Please do not run away screaming in terror! It is not that bad!). Adding this to the mix means we can go from a great ECB to an outstanding one!
I would have added ajax to that list, but that is just the bridge that we will use to combine the two methods together. So without further ado, let me introduce you to the code that will get you going!

There are two functions contained in core.js that render the ECB for an item depending on what type it is.
  • AddListMenuItems
  • AddDocLibMenuItems
Both of these functions look for a “custom” version allowing you to write your own items into the ECB.
  • Custom_AddListMenuItems
  • Custom_AddDocLibMenuItems
The functions are javascript functions that support 3 core functions for creating menus:
  • CAMOpt – This creates a menu item
  • CASubM – This creates a submenu item
  • CAMSep – This creates a separator
Combining these functions will allow you to create many different ECB options.


As you can see, you can do quite a bit with the ECB. Some things to remember:

JAVASCRIPT IS YOUR FRIEND!

The functions are all working with the javascript provided by the core.js file and the list view. This means you have access to:

  • The id of the item you are on.(currentItemID)
  • The list of the item you are on.(ctx.listName)


Javascript can then be used to make changes to items or get more information from them by utilizing the SharePoint web services.

JAVASCRIPT IS YOUR ENEMY!

Because the ECB works with javascript there is an important fact that you must be aware of.
You must decide if you want to check for permissions!
There is no javascript solution for item-level security!
You can use the webservices to determine if a user is in a certain group but you can not check the item directly.
To directly check the item you must use custom .Net code in conjunction with the javascript code.

Here is an example:
function Custom_AddListMenuItems(m, ctx) {
CAMOpt(m, "Print Item...", "_printListItem('" + currentItemID + "')", "/_layouts/images/fax.gif");
     return false; 
}

This just adds a Print Item option to the dropdown and will run the _printListItem function when clicked! The return false line tells the calling function from the core.js file to render the rest of the ECB after your code. If you returned true, this would only draw your code!

As I stated earlier, you will only have so much control at the javascript level. You can use .net code to do more, but I will save that for a future post.

Stay Tuned!
 Some time ago I wrote a part 1 post for what I thought would be a several part series on creating a custom ECB. However, I got sidetracked and have decided that it was time to revisit it again. With that said, I will be copying some items from the old post into this one.

    In some ways, you might call me a lazy user when it comes to having to click through multiple forms or pages to perform certain actions. In other ways, you just might say that I did this because it is cool and makes things easier to the end user. I prefer you say the latter, but I will leave that up to you! I say we get to what we are here to discuss!

Wednesday, April 20, 2011

Lookup Count Related

Just a quick post that I will dive into later after more testing but this is how I am using the CountRelated option to count comments for a blog I am building:

<Field Name="NumComments" ReadOnly="TRUE" Type="Lookup" DisplayName="Comments" CountRelated="TRUE" List="Lists/Comments" ShowField="PostTitle" ID="{Guid Here}" StaticName="NumComments" />

So let's say you have a list called posts for a blog or just about any list will do. I created the field NumComments above. It is a lookup field to the Comments list looking for the field PostTitle. This will actually count the number of comments that have the title of the Blog item in the Comments list in the PostTitle field. This is how the comments field in the OOB blog sites in WSS work.

So another use might be to count the number of times certain keywords or categories show up in a list. The main key is that CountRelated attribute. The rest of the field definition is a standard lookup field. So instead of displaying text, it displays a number which is the count of times the lookup shows up in the selected list. So this field is defined in a parent list and the Lists/Comments in the example would be the child list. ShowField is the name of the field that you are looking up that contains the value you want to count. That is pretty cool!

Wednesday, March 9, 2011

Sharepoint Branding With CSS and Javascript Part 3

So here we are in part 3 of my series of posts on creating a CSS based site in WSS. In my first two posts, I went over the master page and the stylesheets used in this solution. In this post, I am going to go over the xml menus and the javascript coding that makes the rest of it all stick together. I will break this into a few sections.

Keep in mind that my solution is just that! It is a Visual Studio 2008 solution for a site definition. As I said earlier though, you do not have to do it this way. With that in mind, to duplicate what I have, you need to at least have the following things done:

  • An images folder in the Master Page Gallery. (It is just a doc library!)
  • The stylesheets you want to use loaded into the Master Page Gallery. (I used 5)
  • A doc library called Scripts in the root of the site.
For the two navigation areas, I used xml files that I placed in the script library mentioned above. This makes changing the menus rather easy. Here is my code for these menus starting with the mainmenu:

  1. <ul>
  2.     <li><a class="hide" href="javascript:loadpage('/default.aspx');">Home</a></li>
  3.     <li><a class="hide" href="javascript:loadpage('/Lists/Tracking/AllItems.aspx');">Tracking</a>
  4.     <ul>
  5.       <li><a href="javascript:loadpage('/Lists/Tracking/Project.aspx');">By Project</a></li>
  6.       <li><a href="javascript:loadpage('/Lists/Tracking/Code.aspx');">By Code</a></li>
  7.       <li><a href="javascript:loadpage('/Lists/Tracking/Engineer.aspx');">By Engineer</a></li>
  8.       <li><a href="javascript:loadpage('/Lists/Tracking/Supervisor.aspx');">By Supervisor</a></li>
  9.       <li><a href="javascript:loadpage('/Lists/Tracking/Reference.aspx');">By Reference</a></li>
  10.       <li><a href="javascript:loadpage('/Lists/Tracking/MyItems.aspx');">My Items</a></li>
  11.     </ul>
  12.   </li>
  13.     <li><a class="hide" href="javascript:loadpage('/Lists/Twds/AllItems.aspx');">Twds</a>
  14.     <ul>
  15.       <li><a href="javascript:loadpage('/Lists/Twds/Project.aspx');">By Project</a></li>
  16.       <li><a href="javascript:loadpage('/Lists/Twds/Code.aspx');">By Code</a></li>
  17.       <li><a href="javascript:loadpage('/Lists/Twds/Engineer.aspx');">By Engineer</a></li>
  18.       <li><a href="javascript:loadpage('/Lists/Twds/Supervisor.aspx');">By Supervisor</a></li>
  19.       <li><a href="javascript:loadpage('/Lists/Twds/Reference.aspx');">By Reference</a></li>
  20.       <li><a href="javascript:loadpage('/Lists/Twds/MyItems.aspx');">My Items</a></li>
  21.     </ul>
  22.   </li>
  23.     <li><a class="hide" href="javascript:loadpage('/Lists/References/AllItems.aspx');">References</a></li>
  24.     <li><a class="hide" href="mailto:evilgenius@fake.net">Contact</a></li>

Yes, it is not complete and there is a cool reason for this. If you look back at the code for the master page, I left the site actions control there but in a hidden div element. Part of the script will use some code to get the href elements of the hidden site settings and add it to the main navigation at the end. I think that is slick don't you!

Here is the code for the side menu:

  1. <div class="sidebarbutton">Tracking</div>
  2.     <div class="sidebarcontent" id="sidebarcontent">
  3.         <ul>
  4.           <li><a href="javascript:loadpage('/Lists/Tracking/Project.aspx');">By Project</a></li>
  5.           <li><a href="javascript:loadpage('/Lists/Tracking/Code.aspx');">By Code</a></li>
  6.           <li><a href="javascript:loadpage('/Lists/Tracking/Engineer.aspx');">By Engineer</a></li>
  7.           <li><a href="javascript:loadpage('/Lists/Tracking/Supervisor.aspx');">By Supervisor</a></li>
  8.           <li><a href="javascript:loadpage('/Lists/Tracking/Reference.aspx');">By Reference</a></li>
  9.           <li><a href="javascript:loadpage('/Lists/Tracking/MyItems.aspx');">My Items</a></li>
  10.         </ul>
  11.     </div>
  12. <div class="sidebarbutton">Twds</div>
  13.     <div class="sidebarcontent" id="sidebarcontent">
  14.         <ul>
  15.           <li><a href="javascript:loadpage('/Lists/Twds/Project.aspx');">By Project</a></li>
  16.           <li><a href="javascript:loadpage('/Lists/Twds/Code.aspx');">By Code</a></li>
  17.           <li><a href="javascript:loadpage('/Lists/Twds/Engineer.aspx');">By Engineer</a></li>
  18.           <li><a href="javascript:loadpage('/Lists/Twds/Supervisor.aspx');">By Supervisor</a></li>
  19.           <li><a href="javascript:loadpage('/Lists/Twds/Reference.aspx');">By Reference</a></li>
  20.           <li><a href="javascript:loadpage('/Lists/Twds/MyItems.aspx');">My Items</a></li>
  21.         </ul>
  22.     </div>
  23. <div class="sidebarbutton">Information</div>
  24. <div class="sidebarcontent" id="sidebarcontent">
  25.   <table width="100%">
  26.     <tr>
  27.       <td>Title:</td>
  28.       <td>Totally Fake Title!</td>
  29.     </tr>
  30.     <tr>
  31.       <td>Designed By:</td>
  32.       <td>A wickedly cool guy!</td>
  33.     </tr>
  34.     <tr>
  35.       <td>Functional POC:</td>
  36.       <td>
  37.         <a href="mailto:evilgenius@fake.net">Evil Genius (At cox.net)</a>
  38.       </td>
  39.     </tr>
  40.     <tr>
  41.       <td>Technical POC:</td>
  42.       <td>
  43.         That same cool guy.
  44.       </td>
  45.     </tr>
  46.   </table>
  47. </div>

This is the code for the accordion style menu on the right. It is neat and again can be changed as needed. The stylesheets handle the formatting for this.

Now let's go over the javascript code!

  1. var source;
  2. var test = null;
  3. var loc = null;
  4. var viewpage = null;
  5. var viewname;
  6. var reqXML;
  7. var menuhtml;
  8. $(document).ready(function() {
  9.     source = window.location;
  10.     test = new String(window.location);
  11.     /* ---------------------------------------- Begin Menu Section ------------------------------------------------------------*/
  12.     // Get and load the menu data
  13.     lnk = L_Menu_BaseUrl + "/Scripts/mainmenu.xml";
  14.     req = new ActiveXObject("Microsoft.XMLHTTP");
  15.     if (req) {
  16.         req.open("GET", lnk, false);
  17.         req.send();
  18.         menuhtml = req.responseText;
  19.     }
  20.     // Can we add site settings links to the menu?
  21.     menuhtml += "<li><a class='hide' href='#'>Site Settings</a>";
  22.     menuhtml += "<ul>";
  23.     $("menu[id*='FeatureMenuTemplate1'] ie\\:menuitem").each(function() {
  24.         menuhtml += "<li><a href=\"javascript:" + $(this).attr('onMenuClick') + "\"; >" + $(this).attr('text') + "</a></li>";
  25.     });
  26.     menuhtml += "</ul></ul>";
  27.     $("#menu").html(menuhtml);
  28.     lnk2 = L_Menu_BaseUrl + "/Scripts/sidemenu.xml";
  29.     req2 = new ActiveXObject("Microsoft.XMLHTTP");
  30.     if (req2) {
  31.         req2.open("GET", lnk2, false);
  32.         req2.send();
  33.         $("#sidemenu").html(req2.responseText);
  34.     }
  35.     $('.sidebarbutton').click(function() {
  36.         $('.sidebarcontent').slideUp('normal');
  37.         if($(this).next().is(':hidden') == true) {
  38.             $(this).next().slideDown('normal');
  39.         }
  40.     });
  41.     $('.sidebarbutton').mouseover(function() {
  42.         $('.sidebarcontent').slideUp('normal');
  43.         if ($(this).next().is(':hidden') == true) {
  44.             $(this).next().slideDown('normal');
  45.         }
  46.     });
  47.     $(".sidebarcontent").hide();
  48.     /*------------------------------------------ End Menu Section -------------------------------------------------------------*/
  49.     /*----------------------------------------- Begin Style Section -----------------------------------------------------------*/
  50.     // Can we determine our useable resolution?
  51.     var h = screen.availHeight;
  52.     var w = screen.availWidth;
  53.     h = h - 350;
  54.    
  55.     // load a stylesheet based on the resolution
  56.     if (w < 1024) {
  57.         switchstyle("default");
  58.     }
  59.     if (w >= 1024 && w < 1280) {
  60.         switchstyle("mcurves1024");
  61.     }
  62.     if (w >= 1200 && w < 1440) {
  63.         switchstyle("mcurves1200");
  64.     }
  65.     if (w >= 1440 && w < 1600) {
  66.         switchstyle("mcurves1440");
  67.     }
  68.     if (w >= 1600) {
  69.         switchstyle("mcurves1600");
  70.     }
  71.     /*------------------------------------------ End Style Section ------------------------------------------------------------*/
  72.     viewname = "";
  73.     viewname = $("td[id*='onetViewSelector']").text();
  74.     if (viewname == "") { viewname = "All"; }
  75.     try {
  76.         var $table = $("TABLE[Summary*='MCurveData']");
  77.         $table.wrap("<div id='wptable1' style='height:" + h + "px;overflow:scroll;'></div>");
  78.         $("TR.ms-viewheadertr:first", $table).addClass("fixedheader");
  79.     }
  80.     catch (e) { }
  81.     $("#printalert").dialog({
  82.         bgiframe: true,
  83.         autoOpen: false,
  84.         resizable: false,
  85.         modal: true,
  86.         position: ['center', 'center'],
  87.         overlay: { backgroundColor: '#ffdddd', opacity: 0.8 },
  88.         height: 150,
  89.         width: 475,
  90.         closeOnEscape: true,
  91.         title: 'ALERT',
  92.         buttons: { "OK": function() { $(this).dialog("close"); } }
  93.     });
  94.     window.onbeforeprint = function() { beforeprint(); }
  95. });
  96. function switchstyle(style) {
  97.     var z, tag;
  98.     for (z=0, tag = document.getElementsByTagName("link"); z < tag.length; z++)
  99.     {
  100.         if ((tag[z].rel.indexOf("stylesheet") != -1) && tag[z].title)
  101.         {
  102.             tag[z].disabled = true;
  103.             if (tag[z].title == style)
  104.             {
  105.                 tag[z].disabled = false;
  106.             }
  107.         }
  108.     }
  109. }
  110. function sitesettings() {
  111.     var smenua = $("menu[id*='FeatureMenuTemplate1']").attr("id");
  112.     var smenub = $("a[id*='SiteActionsMenu']").attr("id");
  113.     MMU_Open(byid(smenua), MMU_GetMenuFromClientId(smenub), event, false, null, 0);
  114. }
  115. function SearchIt() {
  116.     var k = $("#search-text").val();
  117.     var lnk = L_Menu_BaseUrl + "/_layouts/searchresults.aspx?k=" + k;
  118.     window.location = lnk;
  119. }
  120. function loadpage(lnk) {
  121.     lnk = L_Menu_BaseUrl + lnk;
  122.     window.location = lnk;
  123. }
  124. function beforeprint() {
  125.     $("#printalert").dialog("open");
  126. }

So there you have the code! As you can see I show how I add the site settings links to the main menu and then I load the accordion menu and setup the "buttons" to allow the accordion functionality to work. This could be modified to just a click event if desired. The next thing that I do here is check the width of the screen to determine which of the loaded stylesheets should be set as active. Remember that I load all of them in the master page. This just determines which one to set.

There are some other things here that I use to make things look better. In list view pages, I try to keep the list views from scrolling off the page to ensure that the footer is always at the bottom and that items do not scroll under it. You will find a try/catch block here that wraps the list view in a scrollable div and also sets the header row to fixed so that it functions similar to locked rows in Excel. This function was for a specific listview and should be changed or removed. I did not take the time to set a fixed height for the content div so I just used some math to sort of guess how tall it should be.

The other function of note that I left in here is the sitesettings function. If you wanted to create a button or some other method to run it, the function opens the real site actions where it is so in my case I chose not to add this.

The styles that I used are carried maybe a bit too far as the toolbar dropdowns are black with red hover like the main menu, but I wanted to show that you could do it and I kind of like it!

Well, that is it for this series of posts! I would really like your comments and feedback!