Introduction
About a week ago, I was tasked with a project to allow our SharePoint users to build queries and search their lists based on specific criteria. As I was researching, I started to ponder on how I'm going to display input controls to users so that they could punch in their criteria for searching their lists. Now, I was very overwhelmed because I didn't want to have to build all these input controls for each column that they wanted to query. Why should I? The SharePoint engineers already did that with the edit controls they provide when you create a new list item. So I thought, "Hey, this might not be that bad, the engineers probably provided me a way to do use these controls - right?" Ummm... Kinda, after a little hacking. (Microsoft is probably going to hate me.) After going through all the pain of research and development, I've created an ASP.NET web control that will allow you to pass in a list and a field so that an edit control can be rendered to the page.
Background
Let's go ahead and take a look at where I started. I was reluctant to find any information out there on how to use these edit controls for what I wanted. Most people just wanted to replicate SharePoint's "Edit Properties" page so that they could override some of the controls. So, I was stuck, and couldn't find anything except MSDN's unhelpful description of the SPField.FieldRenderingControl
property. This is the property that supplies the web control that is rendered to the page when a user edits a list item. So, I resorted to figuring out how this control works on my own using my handy tool Reflector which was built by RedGate. Here's a quick glimpse on how to get to this control:
SPList list = SPContext.Current.Web.Lists["myList"];
SPField spField = list.Fields.GetFieldByInternalName("myFieldsInternalName");
BaseFieldControl bfc = spField.FieldRenderingControl;
If you go through the collection of fields, you'll find that all the fields are of different types. In other words, the SPField
is a base class, and all your different SharePoint field types are just classes that derive from it. Such as SPFieldText
, SPFieldLookup
, SPFieldChoice
, and so on. Each of these classes overrides the FieldRenderingControl
property, and returns a different edit control that allows the user to modify the field value that is stored in the database. Each derived field type may also override methods like:
object GetFieldValue(string value);
string GetValidatedString(object value);
This method is used to take a string and convert it into an object that can be modified by the BaseFieldControl
that is overridden. For instance, the SPFieldNumber
type that returns a double
.
public override object GetFieldValue(string value)
{
if (!string.IsNullOrEmpty(value))
{
return Convert.ToDouble(value, CultureInfo.InvariantCulture);
}
return value;
}
To use this method, we simply do the following (our spField
object is of type SPFieldNumber
):
string strDouble = "56.56";
object objDouble = spField.GetFieldValue(strDouble);
BaseFieldControl bfc = spField.FieldRenderingControl;
bfc.ListId = list.Id;
bfc.FieldName = spField.InternalName;
bfc.ItemId = spItem.Id;
if(!Page.IsPostBack && bfc != null)
{
bfc.Value = objDouble;
}
this.Controls.Add(bfc);
And then, during our postback, we can get our string value back by doing the following:
string strDouble = spField.GetValidatedString(bfc.Value);
If the GetValidatedString
method isn't overridden by the derived control, then it will simply just do a ToString()
against the object. This is the case for the SPFieldNumber
.
Hopefully, I didn't mislead you because working with the BaseFieldControl
is not as easy as it appears above. I cut out a lot of code to just simply show you how the BaseFieldControl
ties in with the SPField
.
The Problem
The biggest problem with the SharePoint engineers' design is that the BaseFieldControl
is completely tied with a ListItem
. In my case, we don't have an item, so what are we to do? The second problem is that you need AddItem
permissions to the list to use these controls. So, how will we get around that so viewers can use the edit controls to build search criteria? They are not actually modifying an item, so why shouldn't they be allowed to query the list? The third problem is that the BaseFieldControl
manipulates the visibility of the editing controls based on the properties of the SPField
object. Such as Sealed
, ReadOnly
, Required
, and Hidden
. List fields like Created By may be hidden, but we still want users to be able to use the edit controls to create values for searching. The final problem is that some SPField
types don't even provide a BaseFieldControl
at all. These are fields like ID
or Computed
that don't need users to change their values. This is understandable, but it created an obstacle for me.
The Solution
The solution here uses a lot of Reflection to hack through the BaseFieldControl
. I know this is probably going to rub a lot of people the wrong way, but it beats having to build all of these controls over again. This solution saved me a ton of time, and hopefully it may save you some too.
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint;
using System.Reflection;
using System.Web.UI;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Runtime.Remoting.Messaging;
using System.Globalization;
namespace Suddenlink.SharePoint.WebControls
{
public class SPFieldRenderer : Panel
{
private BaseFieldControl _bfc = null;
private SPList _list = null;
private SPField _spField;
private TextBox _tbox = null;
private string _value = null;
private bool _isSearch = true;
private Type _tList;
private Type _tFieldMetaData = null;
private Type _tSPField = null;
private Type _tSPContext = null;
private MethodInfo _miSetBoolValue = null;
private MethodInfo _miSetFieldAttributeValue = null;
private FieldInfo _fiSpField = null;
private FieldInfo _fiContextItem;
private FieldInfo _fiContextItemIdSet;
private FieldInfo _fiListPermission;
public SPFieldRenderer()
{
_tFieldMetaData = typeof(FieldMetadata);
_fiSpField = _tFieldMetaData.GetField("m_fld",
BindingFlags.Instance | BindingFlags.NonPublic);
_tSPField = typeof(SPField);
_tSPContext = typeof(SPContext);
_miSetBoolValue = _tSPField.GetMethod("SetFieldBoolValue",
BindingFlags.Instance | BindingFlags.NonPublic);
_miSetFieldAttributeValue = _tSPField.GetMethod("SetFieldAttributeValue",
BindingFlags.Instance | BindingFlags.NonPublic);
_fiContextItem = _tSPContext.GetField("m_item",
BindingFlags.Instance | BindingFlags.NonPublic);
_fiContextItemIdSet = _tSPContext.GetField("m_isItemIdSet",
BindingFlags.NonPublic | BindingFlags.Instance);
_tList = typeof(SPList);
_fiListPermission = _tList.GetField("m_EffectivePermMask",
BindingFlags.Instance | BindingFlags.NonPublic);
}
public SPList List
{
get
{
return _list;
}
set
{
_list = value;
}
}
public SPField Field
{
get
{
return _spField;
}
set
{
_spField = value;
}
}
public string Value
{
get
{
if (_bfc != null)
{
object value = _bfc.Value;
if (value != null)
{
return _spField.GetValidatedString(value);
}
else
return null;
}
else if (_tbox != null)
return _tbox.Text;
else
return _value;
}
set
{
_value = value;
}
}
public bool IsSearch
{
get
{
return _isSearch;
}
set
{
_isSearch = value;
}
}
public bool IsValid
{
get
{
if (_bfc != null)
return _bfc.IsValid;
else
return true;
}
}
public void Validate()
{
if (_bfc != null)
_bfc.Validate();
}
protected override void OnInit(EventArgs e)
{
base.OnLoad(e);
SPField spField = null;
using (SPWeb web = _list.ParentWeb)
{
SPContext renderContext = SPContext.GetContext(this.Context, 0, _list.ID, web);
SPItem impersonateItem = _list.Items.Add();
_fiContextItem.SetValue(renderContext, impersonateItem);
_fiContextItemIdSet.SetValue(renderContext, true);
SPList currList = renderContext.List;
if (!currList.DoesUserHavePermissions(SPBasePermissions.AddListItems))
{
SPBasePermissions perms =
(SPBasePermissions)_fiListPermission.GetValue(currList);
perms |= SPBasePermissions.AddListItems;
_fiListPermission.SetValue(currList, perms);
}
spField = GetField();
_miSetBoolValue.Invoke(spField, new object[2] { "Sealed", false });
_miSetBoolValue.Invoke(spField, new object[2] { "Hidden", false });
_miSetBoolValue.Invoke(spField, new object[2] { "Required", false });
_miSetBoolValue.Invoke(spField, new object[2] { "ReadOnly", false });
spField.ShowInEditForm = true;
if (spField.Type != SPFieldType.Integer &&
spField.Type != SPFieldType.File &&
spField.Type != SPFieldType.Computed &&
spField.Type != SPFieldType.Calculated &&
spField.Type != SPFieldType.Attachments &&
!spField.Id.Equals(SPBuiltInFieldId._CopySource) &&
!spField.Id.Equals(SPBuiltInFieldId._ModerationStatus))
{
_bfc = spField.FieldRenderingControl;
}
if (_bfc != null)
{
_bfc.ID = this.ID + "spRenderControl";
_bfc.FieldName = spField.InternalName;
_fiSpField.SetValue(_bfc, spField);
_bfc.ControlMode = SPControlMode.New;
_bfc.RenderContext = renderContext;
_bfc.ItemContext = renderContext;
this.Controls.Add(_bfc);
_bfc.Visible = true;
SetValue();
}
else
{
_tbox = new TextBox();
_tbox.TextMode = TextBoxMode.MultiLine;
_tbox.ID = this.ID + "_tbox";
_tbox.Attributes["style"] = "height:50px;width:300px";
if (!Page.IsPostBack)
{
if (string.IsNullOrEmpty(_value))
_value = spField.DefaultValue;
if (string.IsNullOrEmpty(_value))
_value = string.Empty;
_tbox.Text = _value;
}
this.Controls.Add(_tbox);
}
}
}
private SPField GetField()
{
SPField spField = _spField;
if (_isSearch)
{
Type fieldType = spField.GetType();
if (fieldType == typeof(SPFieldMultiChoice))
{
SPFieldMultiChoice spMultiChoice = (SPFieldMultiChoice)spField;
SPFieldChoice newField = new SPFieldChoice(_list.Fields, spField.Title);
_miSetFieldAttributeValue.Invoke(newField,
new object[2] { "ID", spField.Id.ToString() });
spField = newField;
}
else if (fieldType == typeof(SPFieldLookup))
{
SPFieldLookup spLookup = (SPFieldLookup)spField;
spLookup.AllowMultipleValues = false;
spLookup.DefaultValue = string.Empty;
}
if (spField is SPFieldChoice)
spField.DefaultValue = string.Empty;
}
return spField;
}
private void SetValue()
{
if (_bfc == null || Page.IsPostBack)
return;
SPField spField = GetField();
try
{
object obj = null;
if (!string.IsNullOrEmpty(_value))
{
if (spField is SPFieldDateTime)
{
DateTime dt = DateTime.MinValue;
if (DateTime.TryParse(_value, null,
DateTimeStyles.RoundtripKind, out dt))
obj = dt;
else
obj = DateTime.Now;
}
else if (_isSearch && spField is SPFieldLookup)
obj = spField.GetFieldValue(_value.Replace(";#", ""));
else
obj = spField.GetFieldValue(_value);
}
else
obj = spField.DefaultValueTyped;
if (obj != null)
_bfc.Value = obj;
}
catch { }
}
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
SetValue();
foreach (Control c in this.Controls)
c.Visible = true;
base.Render(writer);
}
}
}
Using the Code
This code snippet simply creates a new instance of SPFieldRenderer
and adds it to the page.
_spField = _list.Fields.GetFieldByInternalName(this.EditField);
if (_spField != null)
{
_control = new SPFieldRenderer();
_control.Field = _spField;
_control.List = _list;
_control.Value = this.InitialValue;
pnlEditContainer.Controls.Add(_control);
}
else
throw new Exception(string.Format("Field {0} could not be found.",
this.EditField));
Points of Interest
I've also built a query builder that uses these controls, and I'll be posting on this at a later time. Let me know what you think.