Wednesday, May 25, 2011

Type-as-you-go Ajax People Search Web Part using SharePoint 2007 Search Services - Part II

After prototyped and compared both solutions, I found that Content Editor Web Part(CEWP) + Javascript seems more suitable for our needs. It's not only more lightweight, interactive and flexible, but also less administration overhead. The product looks like this:

Here is the design diagram:

Pure Javascript and HTML stuff wrapped in a SharePoint CEWP. The core component here is MyXslt transformer, a small Javascript library I released to transform the search service result (XML) into HTML elements. Below is a list of libraries I used:

Javascript libraries

jQuery1.5+
MyXsltXSLT transformer that transforms the search service result XML directly into HTML elements
jQuery UIUI for dialogs
QTipjQuery plugin for tip style people's details
HighlightjQuery plugin for keyword highlighting
MyImgScalejQuery plugin for Image scaling

The solution layout (for commercial reasons, the full source code is not published at this stage):
PeopleSearch
    +---.hg
    +---clientSideSolution
    |   +---cewp
    |   +---release
    |   +---standalone
    |   +---unitTest
    |   \---_layouts
    |       \---1033
    |           +---images
    |           +---js
    |           +---styles
    |           \---xslt
    +---PeopleSearchWebpart
    +---data
    +---script
    +---UnitTest
    +---UserControlTest
    +---WebTest
    +---LoadTest
    \---TestResults

Another thing worth mention is that the load testing results (see above, using Visual Studio 2010's built-in load testing tool) showed the application was very responsive too.

Tuesday, May 24, 2011

Type-as-you-go Ajax People Search Web Part using SharePoint 2007 Search Services - Part I

In my organization, the SharePoint 2007's built-in People Search web part does not meet our needs:
  • Very limited in customizing both the query and the presentation of the results
  • No wild card search out of the box
  • Not very interactive/attractive: no type-as-you-go, no ajax, slow response, etc.
To overcome those limitations and create a more user-friendly and powerful people search solution, there are generally two options: 1) build your own web part (business logic at server-side) or 2) use the shipped Content Editor Web Part with your customized content (processing at client-side). Here are the pros and cons of the two solutions:

Comparison between Web Part Solutions

Server-side Processing Client-side Processing
Pros
  1. Easier server-side code development with C#, eg. SharePoint search API, XML query packet handling, etc
  2. Easier testing and debuging with Visual Studio
  3. Logging and exception handling ability

  1. Assembly free, easier to deploy and administrate
  2. Processing in client, less load/performance impacts
  3. Built-in Ajax
Cons
  1. More work in deployment and administration
  2. Harder Ajax, have to mix ASP.NET user control and jQuery
  3. Processing in server, potential load/performance impacts

  1. Javascripts harder to test, debug and maintain
  2. Harder logging and exception handling

The traditional server-side SharePoint 2007 web part development is actually easy, if you are using the WSPBuilder with Visual Studio 2010. There are quite a few good articles about how to do that, eg. Developing SharePoint WebParts using User Controls and Web Applications by ggalipeau. Some of the contents are a little outdated, but the steps are still applicable. The new layout in Visual Studio now looks like this:

Be careful there are a few catches though, especially the WSPBuilder 1.0.6 generated codes contain some defects. Eg. in xxxReceiver.cs, you have to comment out all contents in FeatureActivated() as SPFeatureReceiver doesn't have FeatureActivated() method. The first line of FeatureDeactivating() this.FeatureDeactivating(properties); must also be removed as this recursive calls will bring you a stack overflow.

Other than those small bugs, WSPBuilder really helps you a lot in focusing on your business logic, and takes off the pain of generating .wsp web part, packaging and deployment, etc. However, as indicated in the above comparison table, the ASP.NET user control was not born with Ajax functionality and the prerequisite of a local SharePoint instance for integration with the development tools is too heavy and dulls the web part development and testing. Well, if you were a Java developer, probably this would remind you of those similar/painful EJB 2.1 days :-)

Tuesday, May 3, 2011

MyXslt v0.2 released

In my previous blog, I provided a temporary fix for jQuery XSLT plugin. However, it is neither complete nor correct due to the plugin's lacking of callback interface during its asynchronous XSLT processing. From David Flanagan's excellent book "JavaScript: The Definitive Guide (5th Edition)", I found a pure Javascript solution and decided to enhance it to suit my needs.

The potential usage of this small Javascript library is calling any web services that return XML then transforming it directly to HTML elements by XSLT. In my case, I created a customized People Search web part in SharePoint 2007 as a replacement for the built-in one, and made complicated search easier, more flexible and interactive. This web part doesn't need any server side programming as it's pure HTML + Javascript contained in SharePoint's provided Content Editor Web Part. For more information about this web part, please stay tuned.

More information here, downloads and source code.

Sunday, February 20, 2011

Fix jQuery plugin xslt.js not working in Firefox 3.x

First of all, thanks Johann for his excellent jQuery XSLT plugin here.

Unfortunately, this one doesn't work with jQuery 1.5 in Firefox 3. One of the reasons is the readyState was changed to 0 after $.ajax loaded the xml and xslt files. Below is my fix by replacing line 84-118 in jquery.xslt.js with the following:
var change = function() {
    if (xm.readyState == xs.readyState && !transformed) {
        var processor = new XSLTProcessor();
        if ($.isFunction(processor.transformDocument)) {
            // obsolete Mozilla interface
            resultDoc = document.implementation.createDocument("", "", null);
            processor.transformDocument(xm.responseXML, xs.responseXML, resultDoc, null);
            target.html(new XMLSerializer().serializeToString(resultDoc));
        }
        else {
            processor.importStylesheet(xs.responseXML);
            resultDoc = processor.transformToFragment(xm.responseXML, document);
            target.empty().append(resultDoc);
        }
        transformed = true;
    }
};

if (str.test(xml)) {
    xm.responseXML = new DOMParser().parseFromString(xml, "text/xml");
}
else {
    xm = $.ajax({ dataType: "xml", url: xml});
}

if (str.test(xslt)) {
    xs.responseXML = new DOMParser().parseFromString(xslt, "text/xml");
}
else {
    xs = $.ajax({ dataType: "xml", url: xslt});
}
change();
return this;
Will contact the author and update the post later...
(Update: the author hasn't responded, but I created a better solution here.)

Ref: xslt.js version 3.2

Thursday, February 17, 2011

jQuery "Object Expected" error in IE

I encountered this problem when prototyping some SharePoint webparts. It actually works well in Firefox and Chrome. Here's the snippet for illustration:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<script type="text/javascript" src="../js/jquery-1.5.min.js"></script>

<script type="text/javascript">

    $(function() { // <=> $(document).ready(function() {
...
By adding one line of code in line 7 before $(function(),
alert(typeof $);
it prompts "undefined", meaning jQuery is not loaded by IE. The solution is quite simple, IE->Tools->Internet Options->Security, by default it is High, either change it to Medium, or use custom level then enable Scripting->Active scripting (IE8). Now it alerts "function" and jQuery is working nifty then.

Ref: jQuery $(document).ready() failing in IE6

Monday, December 13, 2010

JB Renamer v0.3.36 released

JB Renamer (Java Batch Renamer) is my open source (GNU LGPL v3) tool to batch rename files.

Main features:
--------------
1. Add sequence to file name
2. Re-sequence the file name, eg. change the digits of existing sequences
3. Replace part of the file name either literally or by regular expression (Java style)
4. Move part of the file name
5. Test renaming without the real name change
6. Unlimited undo/redo
7. Save file names in a directory to a text file
8. Pre-defined renaming: load a file list to rename

System Requirements:
--------------------
Java 1.6


Download it here.
Website and Source code

Monday, March 8, 2010

GridView in GridView (2) - LINQ Eager Loading

In this blog, I provided a solution for nested GridViews. The problem with that, like other declarative data source bindings, is you have little control of the data source itself hence the database operations might not be optimal.
For example, as we have to use 2 separate data sources, one for each GridView, the ASP.NET engine will first query session data for the outer grid then query transaction logs for the inner grid.However, this actually can be done in one go. Following the pattern I mentioned in this blog to manually control data source object and using eager loading for LINQ, we can have far few SQL queries to the database hence improve the performance.
In .aspx, remove the data sources. As a result of not setting the DataSourceID property of the GridView, we have to add the OnPageIndexChanging event handler for paging.
<asp:GridView ID="gvSession" CssClass="datagrid" runat="server" 
  AllowPaging="True" PageSize="6" AutoGenerateColumns="False" 
  OnPageIndexChanging="gvSession_PageIndexChanging"
  OnRowDataBound="gvSession_RowDataBound" Width="680px">
  <Columns>
    <asp:BoundField DataField="StaffName" HeaderText="Staff Name" />
    <asp:BoundField DataField="SessionId" HeaderText="Session Id" />
    <asp:BoundField DataField="SessionStartTime" HeaderText="Session Start Time" />
    <asp:BoundField DataField="SessionDuration" HeaderText="Session Duration" />
    <asp:TemplateField HeaderText="Tx Logs">
      <ItemTemplate >
        <asp:GridView ID="gvTxLog" runat="server" AutoGenerateColumns="False" Width="100%">
          <Columns>
            <asp:BoundField DataField="Text" HeaderText="Tx Type" ItemStyle-Width="70%" />
            <asp:BoundField DataField="Value" HeaderText="Duration" />
          </Columns>
        </asp:GridView>
      </ItemTemplate>
    </asp:TemplateField>
  </Columns>
</asp:GridView>
In .aspx.cs, bind outer GridView in Page_load, set the inner GridView's data source in outer grid's OnRowDataBound event handler, and implement the OnPageIndexChanging event handler:
ReportDAO _reportDao;

protected void Page_Load(object sender, EventArgs e)
{
    ...
    if (reportTabs.ActiveTab == txViewTab)
    {
        _reportDao = _reportDao ?? new ReportDAO();
        gvSession.DataSource = _reportDao.GetSessions(txtStartDate.Text, txtEndDate.Text,
            int.Parse(ddlStaffs.SelectedValue));
        gvSession.DataBind();
    }
    ...
}

protected void gvSession_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        GridView subView = (GridView)e.Row.FindControl("gvTxLog");
        subView.DataSource = _reportDao._subTables[
            gvSession.PageIndex * gvSession.PageSize + e.Row.RowIndex];
        subView.DataBind();
    }
}

protected void gvSession_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
    gvSession.PageIndex = e.NewPageIndex;
    gvSession.DataBind();
}
The Data Access Object is rather simple. Using ListItem for inner GridView is because I only need 2 values: transaction type name and duration, corresponding to Text and Value DataField of inner GridView's BoundField in .aspx. You can change that to DataTable if more columns required.
public List<List<ListItem>> _subTables = new List<List<ListItem>>();
...
[DataObjectMethod(DataObjectMethodType.Select, false)]
public DataTable GetSessions(string start, string end, int staffId)
{
    if (staffId < 0) // None
        return null;

    ToDate(start, end, out _startDate, out _endDate);
    using (ImportTxDataContext dataContext = new ImportTxDataContext())
    {
        dataContext.EagerLoad();

        // staffId == 0 means all sessions, -1 means none
        var sessions = (from session in dataContext.Sessions
                        where session.StartTime > _startDate && session.EndTime < _endDate &&
                            (session.StaffId == staffId || staffId == 0)
                        select session).ToList();

        DataTable table = new DataTable();
        table.Columns.Add(new DataColumn("StaffName", typeof(string)));
        table.Columns.Add(new DataColumn("SessionId", typeof(int)));
        table.Columns.Add(new DataColumn("SessionStartTime", typeof(string)));
        table.Columns.Add(new DataColumn("SessionDuration", typeof(string)));
        
        foreach (Session session in sessions) // each row
        {
            DataRow row = table.NewRow();
            row["StaffName"] = session.Staff.Name;
            row["SessionId"] = session.Id;
            row["SessionStartTime"] = session.StartTime.ToString(Util.DATE_TIME_FORMAT);
            row["SessionDuration"] = Util.DurationAsString(session.EndTime - session.StartTime);
            table.Rows.Add(row);
            
            List<ListItem> subTable = new List<ListItem>();
            foreach (TxLog log in session.TxLogs)
            {
                subTable.Add(new ListItem(
                    log.TxType.Name, Util.DurationAsString(log.Duration, false)));
            }
            _subTables.Add(subTable);
        }

        return table;
    }
}
EagerLoad() is an extension method for the trick of LINQ eager loading:
public static void EagerLoad(this ImportTxDataContext dataContext)
{
    DataLoadOptions options = new DataLoadOptions();
    options.LoadWith<Session>(r => r.Staff);
    options.LoadWith<Session>(r => r.TxLogs);
    options.LoadWith<ImportTx.DAO.TxLog>(r => r.TxType);
    dataContext.LoadOptions = options;
}
The LINQ generated SQL is like this:
exec sp_executesql N'SELECT [t0].[Id], [t0].[StaffId], [t0].[LocationId], [t0].[StartTime], [t0].[EndTime], [t2].[SessionId], [t2].[TxTypeId], [t2].[Duration], [t3].[Id] AS [Id2], [t3].[Name], [t3].[Description], [t3].[Active], (
    SELECT COUNT(*)
    FROM [dbo].[TxLog] AS [t4]
    INNER JOIN [dbo].[TxType] AS [t5] ON [t5].[Id] = [t4].[TxTypeId]
    WHERE [t4].[SessionId] = [t0].[Id]
    ) AS [value], [t1].[Id] AS [Id3], [t1].[Name] AS [Name2], [t1].[NetworkName], [t1].[ModifiedBy], [t1].[LastModified], [t1].[Active] AS [Active2], [t1].[IsAdmin]
FROM [dbo].[Session] AS [t0]
INNER JOIN [dbo].[Staff] AS [t1] ON [t1].[Id] = [t0].[StaffId]
LEFT OUTER JOIN ([dbo].[TxLog] AS [t2]
    INNER JOIN [dbo].[TxType] AS [t3] ON [t3].[Id] = [t2].[TxTypeId]) ON [t2].[SessionId] = [t0].[Id]
WHERE ([t0].[StartTime] > @p0) AND ([t0].[EndTime] < @p1)
ORDER BY [t0].[Id], [t1].[Id], [t2].[TxTypeId]',N'@p0 datetime,@p1 datetime',@p0='2009-03-01 00:00:00',@p1='2010-03-09 00:00:00'
Without eager loading, it'll be 4 times more queires.
Ref: