Introduction
I've been writing Web Parts for quite some time now, and I came to realize that I have a lot of Web Parts that use CAML queries to pull information from lists. Because of this, our users were unable to actually configure these Web Parts themselves. Instead, they relied upon the administration group to build their queries for them. It was OK for a little while, but as the popularity for Web Parts grew within the company, we grew tired of having to build these queries for them. Since many of my Web Parts use CAML queries, I figured I'd need to find a way to spread this functionality across all of them. That's why I decided to have this feature work just like the List selector dialog that you see in the Content Query Web Part. I'll explain more below.
Background
I'll first talk about how I figured out how to use SharePoint's framework for creating a dialog page in SharePoint.
First, drop a Content Query Web Part into a web part zone on one of your pages. Then, go ahead and modify its properties. Click the "Show items from the following list" radio button and then hit "Browse".
As you can see, it pops up the PickerTreeView.aspx page with a bunch of query strings appended to the end. If you select a list and click OK, then the values will be returned to the text box in your original window. So, let's find out how this happens.
Here's a little trick I like to do. Mouse over the Browse button, and you'll get a tool tip "Open a new window to select a list".
Right click the web page and go to "View Source". Hit Ctrl+F and then type in the tooltip's text. Click "Find", and boom! We are at our button's HTML.
Now, you'll be able to see, when the users click the button, it fires the JavaScript method mso_launchListSmtPicker()
. So, let's hit Ctrl+F and see if we can find this method in the source of this page. And yes, it's in this page, towards the top.
After some analysis of this method, we find that a callback (the callback sets our controls with the returned values from the dialog) method is created and is passed to LaunchPickerTreeDialog
, which must be the method that launches the PickertTreeDialog
window. So, let's try and find it. First, we can Ctrl+F and see if it's in this page, but I'll save you the time and tell you it's not there. So, what will we do? We'll just launch VS and use the script debugger. If you are using IE 7, click Alt and you'll see the classic menu show up. Then, go to View -> Script Debugger -> Open. Then, start up a new instance of VS. The page's HTML will load up along with all the JavaScript files that are referenced. If we look at the list of JavaScript pages referenced, we'll see the PickerTreeDialog.js and we can safely assume the LaunchPickerTreeDialog
method is in that file.
var PickerTreeDlgUrl="/_layouts/PickerTreeView.aspx";
var PickerTreeDlgDimension="resizable:yes;status:no;location:no;menubar:no;help:no";
var L_WarningFailedOperation_TEXT="Do you wish to continue?";
var L_NullSelectionText_TEXT="Please select a target or click cancel";
var MAX_SOURCEID_LENGTH=1024;
var IdSeparator=",";
function LaunchPickerTreeDialog(title, text, filter, anchor,
siteUrl, select, featureId, errorString,
iconUrl, sourceSmtObjectId, callback)
{
var sourceInfo=false;
var sources=null;
if(sourceSmtObjectId !=null &&
sourceSmtObjectId.length > MAX_SOURCEID_LENGTH)
{
sources=sourceSmtObjectId.split(IdSeparator);
if(sources.length > 1)
{
sourceInfo=true;
}
}
var sourceObjectIdAppend=(sourceInfo)? sources[0] : sourceSmtObjectId;
var dialogUrl=TrimLastSlash(siteUrl)+PickerTreeDlgUrl+"?title="+
title+"&text="+text+
"&filter="+filter+
"&root="+anchor+
"&selection="+select+
"&featureId="+featureId+
"&errorString="+errorString+
"&iconUrl="+iconUrl+
"&sourceId="+sourceObjectIdAppend;
commonShowModalDialog(dialogUrl, PickerTreeDlgDimension, callback, null);
}
function HandleOkReturnValues(strDlgReturnValue, strDlgReturnErr)
{
if (strDlgReturnValue[0].indexOf("Error:") >=0)
{
alert( strDlgReturnValue[0].slice(7));
}
else
{
if(strDlgReturnErr.indexOf("Error:") >=0)
{
var promptUser=strDlgReturnErr.slice(7)+"."+L_WarningFailedOperation_TEXT;
if(confirm(promptUser)==0)
return false;
}
setModalDialogReturnValue(window,strDlgReturnValue);
window.top.close();
}
return false;
}
What does this method do? All it really does is prepare some information to be sent into the commonShowModalDialog
method. commonShowModalDialog
is a core.js method that contains all the logic for showing dialogs. All you have to do is give it a URL, some parameters such as the info in PickerTreeDlgDimension
, and a callback method that will be invoked after the user clicks OK or Cancel from the dialog page. Pretty sweet, eh?
Now that we know how to load up the page, let's figure out how information gets sent back from the dialog to our original window. Let's find the PickerTreeView.aspx page in the layouts directory (C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\LAYOUTS) and open it up. After analyzing the page, we can cut out a lot of ASP code and reuse it as a base template for any future dialogs. The main point to get from this is that dialog pages use the Dialog.Master
master page instead of Application.Master
. This master page provides the consistent look and feel of all dialog pages.
<%@ Page Language="C#" AutoEventWireup="true"
MasterPageFile="~/_layouts/dialog.master" CodeBehind="MYCLASS.aspx.cs"
Inherits="Mullivan.SharePoint.Pages.MYCLASS,Mullivan.SharePoint.Pages,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=c37a514ec27d3057" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls"
Assembly="Microsoft.SharePoint, Version=12.0.0.0,
Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<asp:Content ID="Content1"
contentplaceholderid="PlaceHolderDialogHeaderPageTitle"
runat="server">
<asp:Literal runat="server" ID="dialogTitle" />
</asp:Content>
<asp:Content ID="Content2"
contentplaceholderid="PlaceHolderAdditionalPageHead"
runat="server">
<SharePoint:ScriptLink ID="ScriptLink2" language="javascript"
name="core.js" runat="server" />
<script type="text/javascript" language="javascript">
function HandleOkClicked() {
var strDlgReturnValue = new Array(3);
strDlgReturnValue[0] = "value5";
strDlgReturnValue[1] = "Value4";
strDlgReturnValue[2] = "Value3";
var strDlgReturnErr = "";
if(strDlgReturnValue[0] == null || strDlgReturnValue[0].length <= 0)
{
alert(L_NullSelectionText_TEXT);
}
else
{
return HandleOkReturnValues(strDlgReturnValue, strDlgReturnErr);
}
</script>
<SharePoint:FormDigest ID="FormDigest1" runat="server"/>
</asp:Content>
<asp:Content ID="Content3"
contentplaceholderid="PlaceHolderDialogImage"
runat="server">
<asp:Image ID="imgIcon" width="32"
height="32" runat="server" />
</asp:Content>
<asp:Content ID="Content4"
contentplaceholderid="PlaceHolderDialogDescription"
runat="server">
<asp:Literal runat="server" ID="dialogDescription" />
</asp:Content>
<asp:Content ID="Content5"
contentplaceholderid="PlaceHolderHelpLink" runat="server">
</asp:Content>
<asp:Content ID="Content6"
contentplaceholderid="PlaceHolderDialogBodyMainSection"
runat="server">
</asp:Content>
Take a look above and you'll see the method HandleOkReturnValues
. This is the method that we saw above in PickerTreeView.js that calls setModalDialogReturnValue
. This is a core.js method that invokes the callback method provided by mso_LaunchSmtListPicker
. HandleOkClicked
is just a simple method that illustrates how values are passed back. To register it to be called by the OK button of the master page, we need to add the following code-behind to our new dialog page.
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
((DialogMaster)base.Master).OkButton.Click +=
new EventHandler(OkButton_Click);
}
If this doesn't make complete sense to you yet, hopefully, when you use the query builder and look at the code, it will help clear things up.
Installation
GAC the following DLLs (located in the Build folder):
- Mullivan.Shared.dll
- Mullivan.SharePoint.dll
- Mullivan.SharePoint.Pages.dll
- Mullivan.SharePoint.Reminders.dll
- Mullivan.SharePoint.WebParts.dll
Navigate to C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\LAYOUTS.
Copy Build\Layouts\QueryBuilder.aspx to this dir
Copy Build\Layouts\FieldValueDialog.aspx to this dir
Navigate to C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\1033.
Copy Build\Layouts\1033\QueryBuilder.js to this dir
Copy Build\Layouts\1033\MullivanUtility.js to this dir
Copy Build\Layouts\1033\FieldValueDialog.js to this dir
Register Web Parts: register Mullivan.SharePoint.WebParts.dll in the SharePoint Web Config. Go to C:\inetpub\wwwroot\wss\VirtualDirectories\<Port Number>\web.config. Add the following in between the SafeControl
node:
<SafeControl
Assembly="Mullivan.SharePoint.WebParts, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=c37a514ec27d3057"
Namespace="Mullivan.SharePoint.WebParts"
TypeName="*" Safe="True" />
Go to Site Settings -> Webparts -> click New on the menu.
Scroll to the bottom and check Mullivan.SharePoint.WebParts.ListQueryWebPart and Mullivan.SharePoint.WebParts.ListQueryResultsWebPart, and then scroll back to the top and click Populate Gallery.
Restart
Go to Start -> Run and type iisreset and click ok
Using the Query Builder
Let's take a look at the ListQueryWebPart and ListQueryResultsWebPart that I had you install. Go ahead and drop both into a Web Part zone and connect the two Web Parts up. Then, click "Modify SharePoint Web Part" on ListQueryWebPart.
Go ahead and pick a list you want to query by hitting "Browse", and then click the "Build" button for either view or query. You'll see the QueryBuilder window come up. OK.. go ahead and have fun. If you want to test out a query, then make sure you drop a ListQueryResultsWebPart on the page and connect the two Web Parts up.
Clicking the "Build" button for view:
In this dialog, you'll choose all the fields that you want to come back in your list query.
Clicking the "Build" button for query:
Here is the dialog you will use to build your query. The User Input checkbox is a feature that the ListQueryWebPart uses to work like a templated search. Basically, all the conditions that you want to define values for in the ListQueryWebPart need to be set as User Input. Let me show you a screenshot of what the ListQueryWebPart looks like after the above CAML query is saved.
If you define a condition as User Input, then ListQueryWebPart will render an edit control to the page and insert the value into the CAML query when you click "Search". ListQueryWebPart requires at least one User Input condition to work.
If your Web Part uses the User Input feature of the Query Builder, you'll need to parse out the <UserInput>
tags that are used as placeholders for the <Value>
tag that will need to be placed in there. FYI, here is the CAML query that was passed back to ListQueryWebPart by the Query Builder.
<Where>
<And>
<And>
<Contains>
<FieldRef Name='MultiSelect' />
<UserInput />
</Contains>
<Contains>
<FieldRef Name='Column2' />
<UserInput />
</Contains>
</And>
<And>
<Geq>
<FieldRef Name='Created' />
<UserInput />
</Geq>
<Eq>
<FieldRef Name='MultiLookup' LookupId='True' />
<UserInput />
</Eq>
</And>
</And>
</Where>
<OrderBy>
<FieldRef Name='Modified' Ascending='True' />
</OrderBy>
If you choose not to allow the User Input, then it forces you to put a value in during the creation of the CAML query.
If you are not sure what kind of data goes into your column, then just click the Edit Dialog button. This will help you find lookups, users, choices, and etc. It's really handy.
Using the code
Let's take a look at how to use the Query Builder dialog. Let's take a look at some source code first. Open up the ListQueryEdit.cs file in the Mullivan.SharePoint.WebParts project. This is the editor control that is used by ListQueryWebPart. It registers our JavaScript files and then loads controls that have associated buttons that pop up the dialog windows. These windows will then return values back and set the textboxes with them.
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.WebControls;
using System.Web.UI;
using Microsoft.SharePoint;
namespace Mullivan.SharePoint.WebParts
{
public class ListQueryEditor : EditorPart
{
private TextBox _tboxList = null;
private TextBox _tboxViewFields = null;
private TextBox _tboxQuery = null;
private TextBox _tboxPageSize = null;
private RangeValidator _rvPageSize = null;
public ListQueryEditor(string webPartId)
{
this.ID = webPartId + "_ListQueryEditor";
}
protected override void OnInit(EventArgs e)
{
_tboxList = new TextBox();
_tboxViewFields = new TextBox();
_tboxQuery = new TextBox();
_tboxPageSize = new TextBox();
_rvPageSize = new RangeValidator();
_tboxList.ID = "tboxList";
_tboxViewFields.ID = "tboxViewFields";
_tboxQuery.ID = "tboxQuery";
_tboxPageSize.ID = "tboxPageSize";
_rvPageSize.ID = "rvPageSize";
_rvPageSize.ControlToValidate = "tboxPageSize";
_rvPageSize.ErrorMessage =
"Page size must be an integer between 1 and 1000.";
_rvPageSize.MaximumValue = "1000";
_rvPageSize.MinimumValue = "1";
_rvPageSize.Type = ValidationDataType.Integer;
base.OnInit(e);
}
protected override void CreateChildControls()
{
base.CreateChildControls();
#region "HTML"
Literal litBeginHtml = new Literal();
litBeginHtml.Text = @"
<br />
<div>
<table cellspacing=""0""
cellpadding=""0""
border=""0""
style=""width:0px; width:100%;border-collapse:collapse;"">
<tr>
<td>
<div class=""UserSectionTitle"">
<a id=""{0}_MVQueryCategory_IMAGEANCHOR""
TabIndex=""0""
onkeydown='MSOMenu_KeyboardClick(this, 13, 32);'
style='cursor:hand'
onclick=""javascript:MSOTlPn_ToggleDisplay('{0}_MVQueryCategory',
'{0}_MVQueryCategory_IMAGE', '{0}_MVQueryCategory_ANCHOR',
'Expand category: Query', 'Collapse category: Query',
'{0}_MVQueryCategory_IMAGEANCHOR');""
title=""Collapse category: Query"" >
<img id=""{0}_MVQueryCategory_IMAGE""
alt=""Collapse category: Query""
border=""0""
src=""/_layouts/images/TPMin2.gif"" />
</a>
<a TabIndex=""-1""
onkeydown='MSOMenu_KeyboardClick(this, 13, 32);'
id=""{0}_MVQueryCategory_ANCHOR""
style='cursor:hand'
onclick=""javascript:MSOTlPn_ToggleDisplay('{0}_MVQueryCategory',
'{0}_MVQueryCategory_IMAGE', '{0}_MVQueryCategory_ANCHOR',
'Expand category: Query', 'Collapse category: Query',
'{0}_MVQueryCategory_IMAGEANCHOR');""
title=""Collapse category: Query"" > Query
</a>
</div>
</td>
</tr>
</table>
<div id=""{0}_MVQueryCategory"">
<table cellspacing=""0"" border=""0""
style=""border-width:0px;width:100%;border-collapse:collapse;"">";
Literal litEndHtml = new Literal();
litEndHtml.Text = @"
</table>
</div>";
#endregion "HTML"
this.Controls.Add(litBeginHtml);
string listClientId = this.ClientID + "_tboxList";
string viewClientId = this.ClientID + "_tboxViewFields";
string queryClientId = this.ClientID + "_tboxQuery";
string serverUrl = SPContext.Current.Web.ServerRelativeUrl.Replace(
"/", "\\u002f");
string editListJS = string.Format(
"LQWP_LaunchListPicker('{0}','{1}')", listClientId, serverUrl);
string editViewJS = string.Format(
"LQWP_LaunchViewBuilder('{0}','{1}','{2}')",
listClientId, viewClientId, serverUrl);
string editQueryJS = string.Format(
"LQWP_LaunchQueryBuilder('{0}','{1}','{2}')",
listClientId, queryClientId, serverUrl);
AppendEditControl("List"
, "Value that represents the list that is set to be queried."
,_tboxList
,this.Controls
, "Browse..."
, editListJS
, null);
AppendEditControl("View"
, "The fields that you would like to display in the results."
, _tboxViewFields
, this.Controls
, "Build..."
, editViewJS
, null);
AppendEditControl("Query"
, "The CAML query that will be used to search the list."
, _tboxQuery
, this.Controls
, "Build..."
, editQueryJS
,null);
AppendEditControl("Page Size"
, "The amount of items that should be displayed per page."
, _tboxPageSize
, this.Controls
, null
, null
,_rvPageSize);
this.Controls.Add(litEndHtml);
}
protected override void OnPreRender(EventArgs e)
{
ClientScriptManager csm = this.Page.ClientScript;
Type lnType = this.GetType();
if (!csm.IsClientScriptIncludeRegistered(
@"Mullivan.SharePoint.WebParts.JS.Utility.js"))
{
string url = csm.GetWebResourceUrl(lnType,
@"Mullivan.SharePoint.WebParts.JS.Utility.js");
csm.RegisterClientScriptInclude(lnType,
"Mullivan.SharePoint.WebParts.JS.Utility.js",
ResolveClientUrl(url));
}
if (!csm.IsClientScriptIncludeRegistered(
@"Mullivan.SharePoint.WebParts.JS.ListQuery.js"))
{
string url = csm.GetWebResourceUrl(lnType,
@"Mullivan.SharePoint.WebParts.JS.ListQuery.js");
csm.RegisterClientScriptInclude(lnType,
"Mullivan.SharePoint.WebParts.JS.ListQuery.js", ResolveClientUrl(url));
}
if (!csm.IsClientScriptIncludeRegistered(@"PickerTreeDialog"))
{
string url = @"/_layouts/1033/PickerTreeDialog.js";
csm.RegisterClientScriptInclude(lnType, "PickerTreeDialog", url);
}
if (!csm.IsClientScriptIncludeRegistered(@"QueryBuilderDialog"))
{
string url = @"/_layouts/1033/QueryBuilder.js";
csm.RegisterClientScriptInclude(lnType, "QueryBuilderDialog", url);
}
base.OnPreRender(e);
}
private void AppendEditControl(string displayName, string description
, WebControl control, ControlCollection controlCollection
, string editText, string editJS, Control validationControl)
{
Literal litBeginHtml = new Literal();
Literal litEndHtml = new Literal();
controlCollection.Add(litBeginHtml);
controlCollection.Add(control);
if (validationControl != null)
controlCollection.Add(validationControl);
controlCollection.Add(litEndHtml);
litBeginHtml.Text = string.Format(@"
<tr>
<td>
<div class=""UserSectionHead"">
<LABEL FOR=""{0}""
TITLE=""{2}"">{1}
</LABEL>
</div>
<div class=""UserSectionBody"">
<div class=""UserControlGroup"">
<table cellpadding=""0""
cellspacing=""0"" border=""0"">
<tr style=""text-align:left"">
<td>", control.ClientID, displayName, description);
litEndHtml.Text = @"
</td>
</tr>
<tr style=""text-align:right;{0}"">
<td><input type=""button""
value=""{1}""
title=""Click to edit.""
tabindex=""0""
class=""ms-PropGridBuilderButton""
style=""display:inline;
cursor:pointer;width:55px;
text-align:center""
onclick=""javascript:{2}"" />
</td>
</tr>
</table>
</div>
</div>
<div style='width:100%' class='UserDottedLine'>
</div>
</td>
</tr>";
string display = "";
if (string.IsNullOrEmpty(editText))
{
editText = string.Empty;
editJS = string.Empty;
display = "display:none";
}
litEndHtml.Text = string.Format(litEndHtml.Text, display, editText, editJS);
control.CssClass = "UserInput";
control.Style[HtmlTextWriterStyle.Width] = "176px";
}
public override bool ApplyChanges()
{
EnsureChildControls();
ListQueryWebPart lqwp = this.WebPartToEdit as ListQueryWebPart;
if (lqwp == null)
return false;
lqwp.Query = _tboxQuery.Text;
lqwp.ListUrl = _tboxList.Text;
lqwp.ViewFields = _tboxViewFields.Text;
lqwp.PageSize = uint.Parse(_tboxPageSize.Text);
return true;
}
public override void SyncChanges()
{
EnsureChildControls();
ListQueryWebPart lqwp = this.WebPartToEdit as ListQueryWebPart;
if (lqwp == null)
return;
_tboxQuery.Text = lqwp.Query;
_tboxList.Text = lqwp.ListUrl;
_tboxViewFields.Text = lqwp.ViewFields;
_tboxPageSize.Text = lqwp.PageSize.ToString();
}
}
}
Above, you'll see that we registered Mullivan.SharePoint.WebParts.JS.ListQuery.js, so let's take a look at it. This contains our JavaScript methods for using the PickertTreeView
dialog and the query builder dialog.
var lastSelectedListId = null;
function LQWP_LaunchListPicker(clientId, serverUrl) {
var callback = function(results) {
LQWP_SetList(clientId, results);
};
LaunchPickerTreeDialog('CbqPickerSelectListTitle',
'CbqPickerSelectListText',
'listsOnly', '', serverUrl, lastSelectedListId, '', '',
'/_layouts/images/smt_icon.gif', '', callback);
}
function LQWP_SetList(clientId, results) {
var listTextBox = document.getElementById(clientId);
if (results == null
|| results[1] == null
|| results[2] == null) return;
if (results[2] == "") {
alert("You must select a list!.");
return;
}
lastSelectedListId = results[0];
var listUrl = '';
if (listUrl.substring(listUrl.length - 1) != '/')
listUrl = listUrl + '/';
if (results[1].charAt(0) == '/')
results[1] = results[1].substring(1);
listUrl = listUrl + results[1];
if (listUrl.substring(listUrl.length - 1) != '/')
listUrl = listUrl + '/';
if (results[2].charAt(0) == '/')
results[2] = results[2].substring(1);
listUrl = listUrl + results[2];
listTextBox.value = listUrl;
}
function LQWP_LaunchQueryBuilder(listClientId, queryClientId, serverUrl) {
var queryTextBox = document.getElementById(queryClientId);
var listTextBox = document.getElementById(listClientId);
if (listTextBox.value.replace(/^\s+|\s+$/g, "") == "") {
alert("A list must be selected before building a query.");
return;
}
var callback = function(results) {
LQWP_SetQuery(queryClientId, results);
};
var query = queryTextBox.value;
query = Mullivan.Utilities.UrlEncode(query);
QB_LaunchQueryBuilderDialog(serverUrl, listTextBox.value,
null, query, false, true, true, callback);
}
function LQWP_SetQuery(clientId, results) {
var queryTextBox = document.getElementById(clientId);
if (results == null || results.length < 2)
return;
queryTextBox.value = results[1];
}
function LQWP_LaunchViewBuilder(listClientId, viewClientId, serverUrl) {
var viewTextBox = document.getElementById(viewClientId);
var listTextBox = document.getElementById(listClientId);
if (listTextBox.value.replace(/^\s+|\s+$/g, "") == "") {
alert("A list must be selected before building a query.");
return;
}
var callback = function(results) {
LQWP_SetView(viewClientId, results);
};
var view = viewTextBox.value;
view = Mullivan.Utilities.UrlEncode(view);
QB_LaunchQueryBuilderDialog(serverUrl, listTextBox.value,
view, null, true, false, false, callback);
}
function LQWP_SetView(clientId, results) {
var viewTextBox = document.getElementById(clientId);
if (results == null || results.length < 2)
return;
viewTextBox.value = results[0];
}
And voila!
Wow... that's a long article and I'm really tired. Please give feedback.