Tuesday, July 17, 2012

Sorted Dropdown for MatchPoint Form WebPart with Distinct Elements

MatchPoint is a handy framework for creating fast custom solutions. In this post I'll show you how you can bypass some limitations with the Form WebPart.

A customer of mine wanted a sorted select in his form webpart in which he can choose the assigned user of a task list. Something like that:

Example of a sorted Dropdown

1.Attempt - The SPListChoiceProvider

You can do this easily with a ChoiceField in the Form WebPart. As provider (Source of the Dropdown) a SPListChoiceProvider would be a logical choice.

In the SPListChoiceProvider you can select the source list, a key and a value column for the dropdown. Additionally you can also select "UseDistinctValues" to remove duplicate entries.

One drawback or missing feature here is the ability to choose an order for the elements. Another is that you can only choose a single field as key. There is no possibility to combine fields.


2. Attempt - The ExpressionChoiceProvider

My second attempt was to use the ExpressionChoiceProvider. With this provide you can enter a MatchPoint Expression as source. So I tried:

Web.Lists["Jeve Masterplan 2011"].Where("AssignedTo", "!=", null).Select({"AssignedTo"}).OrderBy("AssignedTo", "Ascending").Distinct()

The result was something like that:



The order was correct bu it seems that the Distinct() Method of Linq is not working as expected. I couldn't find any solution here.

3.Attempt - Composite Field

The third attempt was to select a composite field. A composite can be designed as you want. So I selected the composite field gave it the name "fldAssignedTo" and checked the "EnableSelection" checkbox.

As provider I chose the ListDataProvider.

To design the composite I chose the XslTransformer because it's much more powerfull than the PatternTransformer. The Composite field gives you a lot flexibility. So the first thing I made was to map fields, so I can use them in the XSL.

Mappings


Within the Composite Field  you can mark selectable html elements  with the attribute Selectable="1". With Selectable="1" the html elements get clickable and you can connect them to other MatchPoint WebParts. Herefore EnableSelection must be activated on the field properties.

Then I tried the following XSL Code. It creates a simple small div with clickable p elements.

 <xsl:stylesheet version="1.0" .. >  
  <xsl:key name="Persons" match="//Row" use="Person" />  

  <xsl:template match="/">  
   <div style="height:60px; width:200px; overflow:auto">     
    <xsl:for-each select="//Row[generate-id(.) = generate-id(key('Persons', Person))]">  
     <xsl:sort select="Person"/>  
     <xsl:apply-templates select="." />  
    </xsl:for-each>  
   </div>  
  </xsl:template>  

  <xsl:template match="Row">    
   <p Selectable="1"><xsl:value-of select="Person" /></p>   
  </xsl:template>  

 </xsl:stylesheet>  


I used the Muenchian Method to group the the items by Person. So I could remove distinct values. With the sort within the foreach loop I could also sort the selected items.

The cool part was that the elements were now distinct, ordered and selectable. The bad part was, that I connected the form with another webpart and when I selected an element in the form, wrong items were shown in the connected webpart. Without grouping and ordering there were no problems.

Then I look a little into the javascript of the MatchPoint framework and realized that the Composite WebPart creates an array rowIds with the Item IDs of the returned XML. When you change the order of the items in the XML the items and the associated ids in the array are not properly connected anymore.

Row IDs

Another problem is also the Selectable attribute. The Composite is not designed to create a dropdown in your form. Therefore you've other fields. The problem is that the Selectable attribute has no effect on option fields in html. (in Internet Explorer)

 <xsl:stylesheet version="1.0" .. >  
  <xsl:key name="Persons" match="//Row" use="Person" />  
  <xsl:template match="/">  
   <select>     
    <xsl:for-each select="//Row[generate-id(.) = generate-id(key('Persons', Person))]">  
     <xsl:sort select="Person"/>  
     <xsl:apply-templates select="." />  
    </xsl:for-each>  
   </select>  
  </xsl:template>  
  <xsl:template match="Row">    
   <option Selectable="1"><xsl:value-of select="Person" /></option>   
  </xsl:template>  
 </xsl:stylesheet>  


Internally the MatchPoint Framework searches all html elements with the Selectable attribute and adds them a click event. A click event on option element has unfortunately no effects.

I was so close to solve this problem, but none of the above methods could be use

4 Attempt - JavaScript Hacking

This is always the last way I help me out. The theory is to use the underlying MatchPoint Framework and add some more features in the way that I don't change the default behaviour. The decorator pattern is here answer.

My idea was to add a "Changeable" attribute to mark a select box as an item, which has elements connected to other webparts. So I extended the MP.Composite function with my abilities:


 var OriginalMPComposite = MP.CompositeWebPart;  
 MP.CompositeWebPart = function() {  
   OriginalMPComposite.apply(this, arguments);

   this.Setup = function() {       
     this.BindEventHandlers();  
     this.BindChangeEvent();  
   }

   this.BindChangeEvent = function() {  
     this.$container = $(this.Control).find('#RowContainer');  
     if (this.EnableSelection)  
     {  
       var me = this;  
       this.$container.find("[Changeable]").each(function()  
       {   
         var $row = $(this);            
         var rowId = this.options[0].value;    
         $row.change($$.Delegate.Create(me, me.Row_Change));            
       });  
     }  
   }

   this.Row_Change = function(e) {  
     var $row = $(e.currentTarget);  
     var rowId = $row.val();  
     if (this.SelectedRowId == rowId)  
     {  
       this.SelectedRowId = null;  
     }  
     else  
     {  
       this.SelectedRowId = rowId;  
     }  
     var me = this;  
     this.Callback.SelectionChanged(function() { MP.ConnectionManager.NotifyConsumers(me, "SelectedRow"); });   
     RefreshCommandUI();  
   }    
 }  

In this JavaScript I call the original function an extend the main function with the BindChangeEvent and Row_Change methods.

You can add this JavaScript somewhere in the site or directy into the XsltTransformer. Now you have only mark the select with the Changeable attribute and add the Item IDs into the value of the options.

Now add a Form WebPart to your page and select the Composite Field. Choose a provider for your field. Don't forget to check the "EnableSelection" Checkbox on your field.


Now add a XlsTransformer for your field and add two mappings.



Add the following XSLT to your transformer. Change the element names if you used other mapping names.

 <xsl:key name="Persons" match="//Row" use="Person" />  
  <xsl:template match="/">  
   <select Changeable="1">  
    <option value=""></option>  
    <xsl:for-each select="//Row[generate-id(.) = generate-id(key('Persons', Person))]">  
     <xsl:sort select="Person"/>  
     <xsl:apply-templates select="." />  
   </xsl:for-each>  
   </select>  
   <script type="text/javascript">  
    var OriginalMPComposite = MP.CompositeWebPart;  
    MP.CompositeWebPart = function() {  
   OriginalMPComposite.apply(this, arguments)  
   this.Setup = function() {       
      this.BindEventHandlers();  
      this.BindChangeEvent();  
     }  
   this.BindChangeEvent = function() {  
      this.$container = $(this.Control).find('#RowContainer');  
      if (this.EnableSelection)  
      {  
       var me = this;  
       this.$container.find("[Changeable]")  
         .each(function()  
         {                    
           var $row = $(this);            
           var rowId = this.options[0].value;    
           $row.change($$.Delegate.Create(me, me.Row_Change));            
         });  
      }  
     }  
   this.Row_Change = function(e)  
   {  
    var $row = $(e.currentTarget);  
    var rowId = $row.val();  
    if (this.SelectedRowId == rowId)  
    {  
     this.SelectedRowId = null;  
    }  
    else  
    {  
     this.SelectedRowId = rowId;  
    }  
    var me = this;  
    this.Callback.SelectionChanged(function() { MP.ConnectionManager.NotifyConsumers(me, "SelectedRow"); });   
    RefreshCommandUI();  
   }    
 }  
 </script>    
  </xsl:template>  
  <xsl:template match="Row">    
   <option value="{ItemID}"><xsl:value-of select="Person" /></option>   
  </xsl:template>  


Now you have an ordered dropdown with distinct items which can be selected and connected to other webparts. You can also have other text in the option elements or sort in the order and with the field you want.

Hint: 
Because I've a user field  I used the following Expression in connected WebParts in conjunction with the contains operator:

ConnectionData.fldAssignedTo.SelectedRow.ListItem.AssigendTo.ToString().Split({"#"})[1]





No comments: