Friday, October 12, 2007

The Multi-line Text box and its Malcontents - Part I

When using web browser enabled (InfoPath Forms Services) InfoPath templates that have been published to a SharePoint 2007 site, multi-line textboxes will only display with the height that was specified at design time. If the text box contains text that overflows the design size of the control, a scrollbar appears, and only the visible portion will print.

The alternative is the rich textbox control, however this requires a data source element of the xhtml data type, which may not always be appropriate (e.g. users can copy any html into the field), or available (e.g. when using 3rd party schemas). Fields using the rich textbox control are not editable in browsers other that Microsoft IE. In addition, using this data type can lead to complications when using forms for workflow related tasks. Xhtml data will not serialize to binary by default, requiring a custom serialization routine.

Despite these potential drawbacks, if you want your multi-line text to always print without clipping, you're pretty much going to be stuck with xhtml. What would be great is if the multi-line dynamically expanded as it can be made to do in the InfoPath client. It's a bit of a mystery why Microsoft didn't include this sort of functionality in the Forms Services rendering - after all the rich text box has this functionality and works well. The code below demonstrates how such functionality might work (in IE). In Part II, I'll show that it might be possible to modify the javascript that renders the InfoPath forms on the client to provide similar functionality, and why you probably won't want to do this.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title></title>
<style type="text/css">
    .textbox  {border-style:solid;border-width:1px;}
    .textbox1 {width:400px; height:100px;}
    .textbox2 {width:400px; height:100px;}
</style>
</head>
<body>
<div id="objHtmlContainer"></div>
<script type="text/javascript">
String.prototype.repeat = function(n) { return new Array(n + 1).join(this); }
var objTextBoxesData = [["TextBox1","textbox textbox1",false,"Fixed Multi-Line ".repeat(20)],["TextBox2","textbox textbox2",true,"Dynamic Multi-Line ".repeat(20)]]
var arrHtmlToInsertBuilder = new Array();
function TextBox_OnAfterCreate(objControl) {
      if(objControl.onkeyup)
            objControl.fireEvent("onkeyup");
}
function TextBox_Resize(obj) {
    var minHeight = obj.getAttribute("minHeight");
    if (minHeight == null) {
        minHeight = obj.offsetHeight;
        obj.setAttribute("minHeight", minHeight);
    }
    if (obj.scrollHeight != obj.clientHeight) {
      var height = obj.offsetHeight + obj.scrollHeight - obj.clientHeight
      obj.style.height = (height < minHeight)? minHeight : height;
    }
}
function TextBox_OnScroll() {
    var e = window.event.srcElement;
    var tr = e.createTextRange();
    if (tr.offsetTop > 0)
        tr.scrollIntoView();
}
function TextBox_OnCutOrPaste() {
    var e = window.event.srcElement;
    window.setTimeout(function(){TextBox_Resize(e);}, 25);
}
function TextBox(objTextBoxData) { 
    this._id = objTextBoxData[0];
    this._class = objTextBoxData[1];
    this._isDynamic = objTextBoxData[2];
    this._value = objTextBoxData[3]; 
}
TextBox.prototype.render = function() {
      arrHtmlToInsertBuilder.push("<textarea id=\"" + this._id + "\" class=\"" + this._class + "\" ");
      if (this._isDynamic)
            arrHtmlToInsertBuilder.push("style=\"overflow:hidden;\" oncut=\"TextBox_OnCutOrPaste();\" onpaste=\"TextBox_OnCutOrPaste();\" onkeydown=\"TextBox_Resize(this);\" onscroll=\"TextBox_OnScroll();\" onkeyup=\"TextBox_Resize(this);\" ");
      arrHtmlToInsertBuilder.push(">");
      arrHtmlToInsertBuilder.push(this._value);
      arrHtmlToInsertBuilder.push("</textarea><br />");
}
TextBox.prototype.doPostCreate = function () {
      var e = document.getElementById(this._id);
    window.setTimeout(function(){TextBox_OnAfterCreate(e);}, 500);
}
var staticTextBox = new TextBox(objTextBoxesData[0]);
var dynamicTextBox = new TextBox(objTextBoxesData[1]);
staticTextBox.render();
dynamicTextBox.render();
var objHtmlContainer = document.getElementById("objHtmlContainer");
objHtmlContainer.innerHTML = arrHtmlToInsertBuilder.join('');
staticTextBox.doPostCreate();
dynamicTextBox.doPostCreate();
</script>
</body>
</html>

Tuesday, September 25, 2007

Maximum Number of Simultaneous Workflows

In a post here on SharePoint 2007 maximum limitations, Harshawardhan Chiplonkar - a member of the SharePoint Developer Support team - gave "workflows that can be run" as 15, clarifiying this by adding that this is the maximum number of simultaneous workflows that can be in memory executing code, not the number of workflow instances in progress in the database.

If 15 sounds like a small number for a "hard limit", you're not alone.

It looks like the 15 limit Harsh mentions might actually be the WorkflowEventDeliveryThrottle, which is 15 by default. This throttle can be modified (see below) - try setting it to a very low number, and you should see that workflow activation slows dramatically.



SPWebService spWebService = SPFarm.Local.Services.GetValue<SPWebService>();
spWebService.WorkflowEventDeliveryThrottle = 20; // default is 15
spWebService.Update();

Friday, September 14, 2007

Locked Workflow

I've periodically come across this SPException: "This task is currently locked by a running workflow and cannot be edited" when using the SPWorkflowTask.AlterTask method, even when it seems that the workflow is not in fact locked, and is instead patiently listening for an OnTaskChangedEvent. It turns out that this exception is thrown when the WorkflowVersion of the task list item is not equal to 1, which, if you believe the error message is the same thing as checking to see if the workflow is locked. Only it isn't - apparently sometimes at least, the Workflow version is non zero and the workflow is not locked (the InternalState flag of the workflow does not include the Locked flag bits). I'm not sure why this is occurring - maybe the error message is misleading - but the following code demonstrates a dodgy sort of a workaround that I've found useful. I've no idea if this is a good idea or not, so please treat with skepticism...

using System;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using System.Collections;
using System.Threading;
 
namespace DevHoleDemo
{
    public class WorkflowTask
    {
        public static bool AlterTask(SPListItem task, Hashtable htData, bool fSynchronous, int attempts, int millisecondsTimeout)
        {
            if ((int)task[SPBuiltInFieldId.WorkflowVersion] != 1)
            {
                SPList parentList = task.ParentList.ParentWeb.Lists[new Guid(task[SPBuiltInFieldId.WorkflowListId].ToString())];
                SPListItem parentItem = parentList.Items.GetItemById((int)task[SPBuiltInFieldId.WorkflowItemId]);
                for (int i = 0; i < attempts; i++)
                {
                    SPWorkflow workflow = parentItem.Workflows[new Guid(task[SPBuiltInFieldId.WorkflowInstanceID].ToString())];
                    if (!workflow.IsLocked)
                    {
                        task[SPBuiltInFieldId.WorkflowVersion] = 1;
                        task.SystemUpdate();
                        break;
                    }
                    if (i != attempts - 1)
                        Thread.Sleep(millisecondsTimeout);
                }
            }
            return SPWorkflowTask.AlterTask(task, htData, fSynchronous);
        }
    }
}

Monday, September 10, 2007

Using a ControlClass to render an EditControlBlock menu

I found this usefull post on a CustomAction feature for list item context menus, but ran into a problem when I wanted to use ControlClass code rather than a UrlAction. This post details the problem, and it seems it can't be done because context menu items don't appear to be rendered on the server as WebControls. Html is generated that includes the UrlAction, which comes from the feature's xml definition.

If you're determined not to redirect to a different page, it is possible to trap for a postback event in another (2nd) custom action implemented as a WebControl on the StandardMenu. E.g. (in the feature definition):



<UrlAction Url="javascript:__doPostBack(&#39;MyEventTarget&#39;, &#39;{ItemId}&#39;);"/>


In the second menu CustomAction WebControl code:

        protected override void OnLoad(EventArgs e)
        {
            this.EnsureChildControls();
            base.OnLoad(e);
            if (this.Page.Request["__EVENTTARGET"] == "MyEventTarget")
            {
                int itemId = Convert.ToInt32(this.Page.Request["__EVENTARGUMENT"]);
            }
        }

Wednesday, August 15, 2007

Sharepoint Class Wrapper

Many sharepoint classes are not available to inherit from because the default contructor is hidden. The following (quick and untested!) code uses the CodeDom to generate a wrapper class.

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Reflection;
using Microsoft.SharePoint;
 
namespace DevHoleDemo.ClassWrapper
{
    class Program
    {
        static void Main(string[] args)
        {
            ClassWrapper w = new ClassWrapper("DevHoleDemo", "SPWebWrapper", typeof(SPWeb));
            string s = w.GetCode();
        }
    }
 
    public class ClassWrapper
    {
        string m_namespace;
        string m_className;
        SortedDictionary<string, int> m_imports;
        SortedDictionary<string, int> m_modules;
        StringBuilder m_sb;
        Type m_t;
 
        public ClassWrapper(string targetNamespace, string targetClassName, object source)
            : this(targetNamespace, targetClassName, source.GetType())
        {
        }
 
        public ClassWrapper(string targetNamespace, string targetClassName, Type t)
        {
            m_namespace = targetNamespace;
            m_className = targetClassName;
            m_t = t;
            m_imports = new SortedDictionary<string, int>();
            m_modules = new SortedDictionary<string, int>();
        }
 
        public string GetCode()
        {
            return GetCode("CS");
        }
 
        public string GetCode(string language)
        {
            Generate(language);
            string results = m_sb.ToString();
            results = results.Replace("void implicit_operator_", "implicit operator ");
            return results;
        }
 
        protected void AddNamespace(Type t)
        {
            if (m_imports.ContainsKey(t.Namespace))
                m_imports[t.Namespace]++;
            else
                m_imports.Add(t.Namespace, 1);
            if (m_modules.ContainsKey(t.Module.Name))
                m_modules[t.Module.Name]++;
            else
                m_modules.Add(t.Module.Name, 1);
        }
 
        protected CodeTypeReference GetCodeTypeReference(Type t)
        {
            AddNamespace(t);
            if (t.IsByRef)
                return new CodeTypeReference(t.Name.TrimEnd('&'));
            else
                return new CodeTypeReference(t);
        }
 
        protected void Generate(string language)
        {
 
            m_sb = new StringBuilder();
            TextWriter tw = new StringWriter(m_sb);
            CodeDomProvider cdp = CodeDomProvider.CreateProvider(language);
            CodeTypeReferenceExpression tre = new CodeTypeReferenceExpression(m_t);
 
            //namespace
            CodeNamespace ns = new CodeNamespace(m_namespace);
 
            //class
            CodeTypeDeclaration class1 = new CodeTypeDeclaration(m_className);
            ns.Types.Add(class1);
            class1.IsClass = true;
 
            //base object field
            CodeMemberField cmf1 = new CodeMemberField(GetCodeTypeReference(m_t), "m_" + m_t.Name);
            class1.Members.Add(cmf1);
 
            //constructor
            CodeConstructor cc = new CodeConstructor();
            cc.Attributes = MemberAttributes.Public;
            CodeParameterDeclarationExpression cpde1 = new CodeParameterDeclarationExpression(GetCodeTypeReference(m_t), m_t.Name);
            cc.Parameters.Add(cpde1);
            CodeFieldReferenceExpression cfre1 = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "m_" + m_t.Name);
            cc.Statements.Add(new CodeAssignStatement(cfre1, new CodeArgumentReferenceExpression(m_t.Name)));
            class1.Members.Add(cc);
 
            //methods
            foreach (MethodInfo mi in m_t.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy))
            {
                if (!mi.IsSpecialName)
                {
                    CodeMemberMethod meth = new CodeMemberMethod();
                    meth.Attributes &= ~MemberAttributes.AccessMask & ~MemberAttributes.ScopeMask;
                    meth.Attributes |= MemberAttributes.Public;
                    if ((mi.Attributes & MethodAttributes.Static) == MethodAttributes.Static)
                        meth.Attributes |= MemberAttributes.Static;
                    if ((mi.Attributes & MethodAttributes.Virtual) != MethodAttributes.Virtual)
                        meth.Attributes |= MemberAttributes.Final;
                    meth.Name = mi.Name;
                    List<CodeExpression> prms = new List<CodeExpression>();
                    foreach (ParameterInfo ri in mi.GetParameters())
                    {
                        CodeParameterDeclarationExpression cpde2 = new CodeParameterDeclarationExpression(GetCodeTypeReference(ri.ParameterType), ri.Name);
                        CodeArgumentReferenceExpression care1 = new CodeArgumentReferenceExpression(ri.Name);
                        CodeDirectionExpression cde = null;
                        if (ri.IsOut)
                        {
                            cpde2.Direction = FieldDirection.Out;
                            cde = new CodeDirectionExpression(FieldDirection.Out, care1);
                        }
                        meth.Parameters.Add(cpde2);
                        if (cde == null)
                            prms.Add(care1);
                        else
                            prms.Add(cde);
                    }
 
                    CodeMethodReferenceExpression cmre1;
                    if ((mi.Attributes & MethodAttributes.Static) == MethodAttributes.Static)
                        cmre1 = new CodeMethodReferenceExpression(tre, mi.Name);
                    else
                        cmre1 = new CodeMethodReferenceExpression(cfre1, mi.Name);
 
                    if (mi.ReturnType == typeof(void))
                    {
                        meth.Statements.Add(new CodeMethodInvokeExpression(cmre1, prms.ToArray()));
                    }
                    else
                    {
                        meth.Statements.Add(new CodeMethodReturnStatement(new CodeMethodInvokeExpression(cmre1, prms.ToArray())));
                    }
                    meth.ReturnType = GetCodeTypeReference(mi.ReturnType);
 
                    class1.Members.Add(meth);
                }
            }
 
            //properties
            foreach (PropertyInfo pi in m_t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy))
            {
                CodeMemberProperty prop = new CodeMemberProperty();
                prop.Name = pi.Name;
                prop.Attributes &= ~MemberAttributes.AccessMask & ~MemberAttributes.ScopeMask;
                prop.Attributes |= MemberAttributes.Public | MemberAttributes.Final;
                List<CodeExpression> indices = new List<CodeExpression>();
                if (pi.GetIndexParameters().Length > 0)
                {
                    foreach (ParameterInfo pii in pi.GetIndexParameters())
                    {
                        CodeParameterDeclarationExpression cpde3 = new CodeParameterDeclarationExpression(GetCodeTypeReference(pii.ParameterType), pii.Name);
                        prop.Parameters.Add(cpde3);
                        CodeArgumentReferenceExpression care3 = new CodeArgumentReferenceExpression(pii.Name);
                        indices.Add(care3);
                    }
 
                }
                prop.Type = GetCodeTypeReference(pi.PropertyType);
                prop.HasGet = pi.GetGetMethod() != null;
                if (prop.HasGet)
                {
                    if (pi.GetIndexParameters().Length > 0)
                        prop.GetStatements.Add(new CodeMethodReturnStatement(new CodeArrayIndexerExpression(cfre1, indices.ToArray())));
                    else
                        prop.GetStatements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(cfre1, pi.Name)));
 
                }
                prop.HasSet = pi.GetSetMethod() != null;
                if (prop.HasSet)
                {
                    if (pi.GetIndexParameters().Length > 0)
                        prop.SetStatements.Add(new CodeAssignStatement(new CodeArrayIndexerExpression(cfre1, indices.ToArray()), new CodePropertySetValueReferenceExpression()));
                    else
                        prop.SetStatements.Add(new CodeAssignStatement(new CodeFieldReferenceExpression(cfre1, pi.Name), new CodePropertySetValueReferenceExpression()));
                }
                class1.Members.Add(prop);
            }
 
            //implicit casts
            CodeMemberMethod castFrom = new CodeMemberMethod();
            castFrom.Name = "implicit_operator_" + m_t.Name;
            castFrom.Attributes &= ~MemberAttributes.AccessMask & ~MemberAttributes.ScopeMask;
            castFrom.Attributes |= MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
            CodeParameterDeclarationExpression cpde4 = new CodeParameterDeclarationExpression(new CodeTypeReference(m_className), m_className);
            castFrom.Parameters.Add(cpde4);
            CodeArgumentReferenceExpression care4 = new CodeArgumentReferenceExpression(m_className);
            CodeFieldReferenceExpression cfre3 = new CodeFieldReferenceExpression(care4, "m_" + m_t.Name);
            castFrom.Statements.Add(new CodeMethodReturnStatement(cfre3));
            castFrom.ReturnType = new CodeTypeReference(typeof(void));
            class1.Members.Add(castFrom);
 
            CodeMemberMethod castTo = new CodeMemberMethod();
            castTo.Name = "implicit_operator_" + m_className;
            castTo.Attributes &= ~MemberAttributes.AccessMask & ~MemberAttributes.ScopeMask;
            castTo.Attributes |= MemberAttributes.Public | MemberAttributes.Final | MemberAttributes.Static;
            CodeParameterDeclarationExpression cpde5 = new CodeParameterDeclarationExpression(new CodeTypeReference(m_t), m_t.Name);
            castTo.Parameters.Add(cpde5);
            CodeArgumentReferenceExpression care5 = new CodeArgumentReferenceExpression(m_t.Name);
            CodeObjectCreateExpression coce = new CodeObjectCreateExpression(new CodeTypeReference(m_className), care5);
            castTo.Statements.Add(new CodeMethodReturnStatement(coce));
            castTo.ReturnType = new CodeTypeReference(typeof(void));
            class1.Members.Add(castTo);
 
            //write imports
            foreach (KeyValuePair<string, int> kvp in m_imports)
            {
                ns.Imports.Add(new CodeNamespaceImport(kvp.Key));
            }
 
            //write modules comments
            ns.Comments.Add(new CodeCommentStatement("References to the following modules must be added:"));
            foreach (KeyValuePair<string, int> kvp in m_modules)
            {
                ns.Comments.Add(new CodeCommentStatement(string.Format("\t {0}", kvp.Key)));
            }
 
            cdp.GenerateCodeFromNamespace(ns, tw, null);
            tw.Close();
        }
    }
}

Tuesday, May 29, 2007

Web Configuration Namespace

If you've opened a sharepoint virtual directory as a file system web site using Visual Studio, watch out for the following gotcha (also discussed here):

If VS updates the web.config file, it will add the a reference to the xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0" namespace in the root configuration element.

This will lead to various "File not found" error messages with the log file showing entries like the following:

Could not load file or assembly 'Microsoft.SharePoint.ApplicationPages, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c'

Remove the namespace reference and the problem goes away.

Wednesday, March 28, 2007

Create Groups Programmatically

Reza Alirezaei has posted a helpful article on creating SharePoint sites with unique role assignments. The following simple demo class puts some of these ideas together. Like Reza's example, the code below changes the built in associated groups by using the spWeb property bag directly, rather than the properties exposed by spWeb - e.g. spWeb.AssociatedGroups.Add(group), or spWeb.AssociatedOwnerGroup = group.

using Microsoft.SharePoint;
 
namespace DevHoleDemo
{
    public static class GroupsHelper
    {
        public enum AssociatedGroupTypeEnum { Owners, Members, Vistors };
 
        public static void AddGroups(SPWeb spWeb, bool copyUsersFromParent)
        {
            spWeb.BreakRoleInheritance(false);
            SPGroup owners = AddGroup(spWeb, AssociatedGroupTypeEnum.Owners, copyUsersFromParent);
            SPGroup members = AddGroup(spWeb, AssociatedGroupTypeEnum.Members, copyUsersFromParent);
            SPGroup vistors = AddGroup(spWeb, AssociatedGroupTypeEnum.Vistors, copyUsersFromParent);
            SPGroup myGroup = AddGroup(spWeb, "My Group", "An example group.", null, null, null, false);
            SetAssociatedGroups(spWeb, new SPGroup[] { owners, members, vistors, myGroup });
        }
 
        public static void SetAssociatedGroups(SPWeb spWeb, SPGroup[] groups)
        {
            string formatString = "";
            object[] ids = new object[groups.Length];
            for (int i = 0; i < groups.Length; i++)
            {
                formatString += string.Format("{{{0}}};", i);
                ids[i] = groups[i].ID;
            }
            spWeb.Properties["vti_associategroups"] = string.Format(formatString.TrimEnd(new char[] { ';' }), ids);
            spWeb.Properties.Update();
        }
 
        public static SPGroup AddGroup(SPWeb spWeb, AssociatedGroupTypeEnum associateGroupType, bool copyUsersFromParent)
        {
            switch (associateGroupType)
            {
                case AssociatedGroupTypeEnum.Owners:
                    return AddGroup(spWeb, spWeb.Name + " Owners", "Use this group to give people full control permissions to the SharePoint site: {0}", "Full Control", "vti_associateownergroup", spWeb.ParentWeb.AssociatedOwnerGroup, copyUsersFromParent);
                case AssociatedGroupTypeEnum.Members:
                    return AddGroup(spWeb, spWeb.Name + " Members", "Use this group to give people contribute permissions to the SharePoint site: {0}", "Contribute", "vti_associatemembergroup", spWeb.ParentWeb.AssociatedMemberGroup, copyUsersFromParent);
                case AssociatedGroupTypeEnum.Vistors:
                    return AddGroup(spWeb, spWeb.Name + " Vistors", "Use this group to give people read permissions to the SharePoint site: {0}", "Read", "vti_associatevisitorgroup", spWeb.ParentWeb.AssociatedVisitorGroup, copyUsersFromParent);
                default:
                    return null;
            }
        }
 
        public static SPGroup AddGroup(SPWeb spWeb, string groupName, string descriptionFormatString, string roleDefinitionName, string associatedGroupName, SPGroup parentAssociatedGroup, bool copyUsersFromParent)
        {
            SPGroup owner = parentAssociatedGroup;
            if (associatedGroupName != "vti_associateownergroup")
                owner = spWeb.SiteGroups.GetByID(int.Parse(spWeb.Properties["vti_associateownergroup"]));
            spWeb.SiteGroups.Add(groupName, owner, null, string.Format(descriptionFormatString, spWeb.Name));
            SPGroup group = spWeb.SiteGroups[groupName];
            if (descriptionFormatString.IndexOf("{0}") != -1)
            {
                SPListItem item = spWeb.SiteUserInfoList.GetItemById(group.ID);
                item["Notes"] = string.Format(descriptionFormatString, string.Format("<a href=\"{0}\">{1}</a>", spWeb.Url, spWeb.Name));
                item.Update();
            }
            if (roleDefinitionName != null)
            {
                SPRoleAssignment roleAssignment = new SPRoleAssignment(group);
                SPRoleDefinition roleDefinition = spWeb.RoleDefinitions[roleDefinitionName];
                roleAssignment.RoleDefinitionBindings.Add(roleDefinition);
                spWeb.RoleAssignments.Add(roleAssignment);
            }
            if (copyUsersFromParent && parentAssociatedGroup != null)
                foreach (SPUser user in parentAssociatedGroup.Users)
                    group.AddUser(user);
            if (associatedGroupName != null)
            {
                spWeb.Properties[associatedGroupName] = group.ID.ToString();
                spWeb.Properties.Update();
            }
            spWeb.Update();
            return group;
        }
    }
}