Infinite scroll widget for Sitefinity supporting sorting and searching

Infinite scroll widget for Sitefinity supporting sorting and searching

Posted on July 15, 2014 0 Comments

The content you're reading is getting on in years
This post is on the older side and its content may be out of date.
Be sure to visit our blogs homepage for our latest news, updates and information.

Sitefinity offers default widgets for all modules which support setting a specific datasource and configuring standard paging. However, more and more web applications present their data using the so called infinite scroll paging – where we use load on demand when the user scrolls to the near end of the data container, causing it to grow with additional content. In this blog post I will show how we can achieve this in Sitefinity, providing search and sorting functionality, as well. For a base we will be using the RadListView with an extender providing the scrolling event functionality described here, from which we will inherit and implement a custom control for Sitefinity front-end. I will be using dynamic module items for datasource with related data field, since this can easily be modified to work with the built in modules.

Here is the main module we will be using:

Here is the related one:

 

Widget Initialization

I will start with the server-side logic. We will implement our widget on the base of a SimpleView, or an empty widget created using Sitefinity Thunder. Since, we will be using client-side binding on the client, we will need to implement a service which would be called to retrieve the data. We will implement a method in the widget, which will query the necessary data and register it as a service, so we could request it client-side.

namespace SitefinityWebApp.InfiniteScrollWidget
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    public class InfiniteScrollWidget : SimpleView
    {
        [OperationContract]
        [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Wrapped)]
        public DataResult GetData(int startIndex, string search, SortModel sort)
        {
        }
    }
}

  

We define the widget class as a Service using the ServiceContract attribute and an operation which is the method called. Defined are several parameters, the http request method, the request response and request formats. We will use POST, since we will send the sort, search and page data, for format – JSON. The implementation of the service ends up with registering it in the Global.asax file of our web application:

static void BootstrapperInitialized(object sender, ExecutedEventArgs e)
        {
            if (e.CommandName == "Bootstrapped")
            {
                SystemManager.RegisterWebService(typeof(SitefinityWebApp.InfiniteScrollWidget.InfiniteScrollWidget),
                    "Public/InfiniteScrollService.svc");
            }
        }

Querying the data

I have built the query in steps depending on the input parameters received. The PageIndex is the page number, the search query is a simple string and the sorting is an object holding the field and the sort order (Ascending, Descending):

 

namespace SitefinityWebApp.InfiniteScrollWidget
{
    public class SortModel
    {
        public string Field { get; set; }
 
        public SortOrder SortOrder { get; set; }
    }
}
namespace SitefinityWebApp.InfiniteScrollWidget
{
    public enum SortOrder
    {
        Ascending,
        Descending
    }
}

The base query is all live items from that dynamic type. Remember to leave all base queries in IQueryable , so the database call will be made at the end when the full query is constructed. When working with dynamic content items collections always use the GetValue<T> method, so the query could be translated by OpenAccess to SQL one. The main idea is to get all items with one call to the database and only these columns (item’s properties) we need.

Part of the method for sample querying:

public DataResult GetData(int startIndex, string search, SortModel sort)
        {
            startIndex = startIndex * PageSize;
 
            var providerName = String.Empty;
            DynamicModuleManager dynamicModuleManager = DynamicModuleManager.GetManager(providerName);
            Type brandType = TypeResolutionService.ResolveType("Telerik.Sitefinity.DynamicTypes.Model.Brands.Brand");
 
            var baseQuery = dynamicModuleManager.GetDataItems(brandType)
                .Where(d => d.Status == Telerik.Sitefinity.GenericContent.Model.ContentLifecycleStatus.Live);
 
            if (!string.IsNullOrWhiteSpace(search))
            {
                // Search in title by default
                var baseSearchQuery = baseQuery.Where(c => c.GetValue<string>("Title").StartsWith(search));
                // If Search, return the searched items count
                var count = baseSearchQuery.Count();
 
                if (sort != null && !string.IsNullOrWhiteSpace(sort.Field))
                {
                    if (sort.SortOrder == SortOrder.Ascending)
                    {
                        if (sort.Field == "Established")
                        {
                            return DateTimeSortAscending(startIndex, sort, baseSearchQuery, count);
                        }
                        else
                        {
                            return StringSortAscending(startIndex, sort, baseSearchQuery, count);
                        }                       
                    }
            }
  }

 Using the base query, we filter the collection further and complete the request with a select:

private static DataResult DateTimeSortAscending(int startIndex, SortModel sort, IQueryable<Telerik.Sitefinity.DynamicModules.Model.DynamicContent> baseSearchQuery, int count = 0)
       {
           var data = baseSearchQuery.OrderBy(c => c.GetValue<DateTime?>(sort.Field))
                  .Skip(startIndex).Take(PageSize)
                  .Select(BrandsItemModel.Convert);
 
           return new DataResult() { Data = data, Count = count };
       }
 
       private static DataResult StringSortAscending(int startIndex, SortModel sort, IQueryable<Telerik.Sitefinity.DynamicModules.Model.DynamicContent> baseSearchQuery, int count = 0)
       {
           var data = baseSearchQuery.OrderBy(c => c.GetValue<string>(sort.Field))
                  .Skip(startIndex).Take(PageSize)
                  .Select(BrandsItemModel.Convert);
 
           return new DataResult() { Data = data, Count = count };
       }

I have made a model of my dynamic item and implemented a method for transferring DynamicContent to the model class, which is used in the above logic for querying the items from the database:

namespace SitefinityWebApp.InfiniteScrollWidget
{
    [DataContract]
    public class BrandsItemModel
    {
        [DataMember]
        public DateTime Established { get; set; }
 
        [DataMember]
        public string Title { get; set; }
 
        [DataMember]
        public string OriginCountry { get; set; }
 
        public DynamicContent OriginCountryItem { get; set; }
 
        [DataMember]
        public Guid Id { get; set; }
 
        public static Func<DynamicContent, BrandsItemModel> Convert
        {
            get
            {
                Func<DynamicContent, BrandsItemModel> convertMethod = ConvertToModel;
 
                return convertMethod;
            }
        }
 
        private static BrandsItemModel ConvertToModel(DynamicContent n)
        {
            var model = new BrandsItemModel()
            {
                Id = n.Id,
                Title = n.GetValue<Lstring>("Title"),
                Established = n.GetValue<DateTime>("Established"),
                OriginCountryItem = n.GetValue<DynamicContent>("CountryOrigin")
            };
 
            if (model.OriginCountryItem != null)
            {
                model.OriginCountry = model.OriginCountryItem.GetValue<Lstring>("Title");
            }
 
            return model;
        }
    }
}

The Database query made, with one selection we get page size number of items:

 

The Infinite Scroll ListView

As I said in the beginning, we will derive from the RadListViewExtender, which implements the logic for handling the scrolling and binding the data after that. However, I have extent it to provide support for searching and sorting. Implementing the widget, we will also have the opportunity to add more controls to the descriptor and manipulate them client-side as well.

The infinite scroll extender class, do not forget to include all scripts and resources from the base, as well as all controls necessary for the client widget:

namespace SitefinityWebApp.InfiniteScrollWidget
{
        [TargetControlType(typeof(RadListView))]
    public class CustomExtender : RadListViewClientBindingExtender
    {
            // add controls properties
 
        protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl)
        {
            ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor("SitefinityWebApp.InfiniteScrollWidget.CustomExtender", targetControl.ClientID);
 
            descriptor.AddProperty("_clientItemTemplate", HtmlEncode(ClientItemTemplate.Trim()));
            DataBindingSettings.DescribeClientProperties(descriptor);
            // Add necessary controls, from the custom ascx of the widget
 
            return new ScriptDescriptor[] { descriptor };
        }
 
        protected override IEnumerable<System.Web.UI.ScriptReference> GetScriptReferences()
        {
            ScriptReference reference = new ScriptReference();
            reference.Path = "CustomExtender.js";
            ScriptReference reference1 = new ScriptReference();
            reference1.Path = "RadListViewClientBindingExtender.js";
 
            return new ScriptReference[] { reference1, reference };
        }
    }
}

Here is the widget full template with the controls for reference, we define our extender as the RadListView extender. For the client binding is used jQuery template:

<telerik:RadListView runat="server" ID="RadListView1" AllowCustomPaging="true">
          <LayoutTemplate>
              <div class="RadListView RadListViewFloated RadListView_Windows7">
                  <div class="rlvFloated" id="MyContainer1">
                      <div id="itemPlaceHolder" runat="server">
                      </div>
                  </div>
              </div>
          </LayoutTemplate>
          <ItemTemplate>
              <div class="rlvI">
                  <ul>
                      <li><i>Title</i>: <b>
                          <%#DataBinder.Eval(Container.DataItem, "Title")%></b> </li>
                      <li><i>Established</i>: <b>
                          <%#DataBinder.Eval(Container.DataItem, "Established")%></b> </li>
                      <li><i>Origin Country</i>: <b>
                          <%#DataBinder.Eval(Container.DataItem, "OriginCountry")%></b> </li>
                  </ul>
              </div>
          </ItemTemplate>
      </telerik:RadListView>
 
      <telerik:CustomExtender runat="server" ID="RadListViewClientBindingExtender1"
          TargetControlID="RadListView1">
          <ClientItemTemplate>
          <div class="rlvI">
             <ul>
                  <li><i>Title</i>: <b>
                      {%=Title%}</b> </li
                 <li><i>Established</i>: <b>
                      {%=Established%}</b> </li>   
                   <li><i>Origin Country</i>: <b>
                      {%=OriginCountry%}</b> </li>                   
              </ul>              
          </div>               
          </ClientItemTemplate>
 
          <DataBindingSettings ClientItemContainerID="MyContainer1" LocationUrl="/Public/InfiniteScrollService.svc"
              MethodName="GetData" />
 
      </telerik:CustomExtender>

The important part is the client-side of the widget, which do most of the work. Remember to declare the right namespace and all descriptor controls’ getters and setters.

Handling the scroll event and decide whether or not to load more data:

_handleScroll: function () {
            // if scroller is dragged to the bottom we request for more data
            var container = this.get_itemContainer()[0];
 
            var visibleHeight = (container.scrollHeight - container.offsetHeight);
            if (this.get_itemContainer().scrollTop() >= (container.scrollHeight - container.offsetHeight)) {
                this._loadData();
            }
        },

The sort and search handlers just set the entered input in the widget properties and call the loadData function, as well.

 

get_searchValue: function () {
    if (this._searchTextBox)
        return this._searchTextBox.value;
 
    var textbox = this.get_searchTextBox();
    this._searchTextBox = textbox;
    return this._searchTextBox.value;
},
 
_handleSearch: function () {
    this._search = this.get_searchValue();
    // when searching restart the page counters and noMoreData flag
    this._currentPageIndex = -1;
    this._noMoreData = false;
 
    // set search message
    this._isSearch = true;
    this.get_searchMessage().innerHTML = "Searching for: " + this._search;
 
    try {
        var container = this.get_itemContainer();
        container.html("");
        this._loadData();
    } catch (e) {
        alert(e);
    }
 
},
 
_handleSort: function () {
    var checked = this.get_sortOrderCheckBox().checked;
    var sortField = this.get_sortTextBox().value;
 
    // Enumeration Descending value is 1
    if (checked) {
        this._sort = { "Field": sortField, "SortOrder": 1 };
    }
    else {
        this._sort = { "Field": sortField, "SortOrder": 0 };
    }
    this._currentPageIndex = -1;
    this._noMoreData = false;
    try {
        var container = this.get_itemContainer();
        container.html("");
        this._loadData();
    } catch (e) {
        alert(e);
    }
 
},

The loadData function reads all properties and construct the http request to the server, remember that we are using JSON as request format:

 

_loadData: function () {
            if (this._isNullOrEmpty(this._locationUrl) || this._isNullOrEmpty(this._methodName) || this._noMoreData)
                return;
 
            var url = String.format("{0}/{1}", this._locationUrl, this._methodName);
 
            var data = { "startIndex": ++this._currentPageIndex, "search": this._search, "sort": this._sort };
            // serialize Data to JSON
            var dataToPost = Sys.Serialization.JavaScriptSerializer.serialize(data);
            // execute the request
            try {
                $telerik.$.ajax({
                    type: "POST",
                    url: url,
                    contentType: "application/json",
                    dataType: "json",
                    data: dataToPost,
                    success: Function.createDelegate(this, this._bindContainer),
                    error: function (error) { alert(error); }
                });
 
            }
            catch (e) {
                throw new Error(e);
            }
        },

If the request is successful, the result is processed and the listview bound with the additional data. Keep in mind the result JSON will be wrapped in an object named after the result method name plus Result and will contain the JSON result:

{"GetDataResult":{"Count":8,"Data":[]}}
_bindContainer: function (result) {
           if (result) {
               var resultObject = result.GetDataResult;
               if (resultObject) {
                   var dataObject = resultObject.Data;
                   var count = resultObject.Count;
                   if (dataObject) {
                       var data = dataObject;
                       if (data && data.length > 0) {
                           var container = this.get_itemContainer();
                           for (var i = 0, len = data.length; i < len; i++) {
                               var dataItem = data[i];
                               var itemHtml = this._constructTemplate(dataItem);
                               jQuery(container).append("clientItemTemplate", dataItem);
 
                           }
                       }
                       else {
                           this._noMoreData = true;
                           var container = this.get_itemContainer();
                           var noDataElement = $("<div>No More Data</div>");
                           noDataElement.css('width', '100%');
                           noDataElement.css('clear', 'both');
                           jQuery(container).append(noDataElement);
                       }
                   }
 
                   if (count && this._isSearch) {
                       this._isSearch = false;
                       this.get_searchMessage().innerHTML += " Count: " + count;
                   }
               }
           }
           else {
               var container = this.get_itemContainer();
               jQuery(container).append("<div style='width:100%; clear:both;'>No Data</div>");
           }
       },

Here is a video demonstration of the sample:

The full source code could be downloaded InfiniteScrollWidget.

Nikola Zagorchev

Nikola Zagorchev is a Tech Support Engineer at Telerik. He joined the Sitefinity Support team in March 2014.

Comments

Comments are disabled in preview mode.
Topics

Sitefinity Training and Certification Now Available.

Let our experts teach you how to use Sitefinity's best-in-class features to deliver compelling digital experiences.

Learn More
Latest Stories
in Your Inbox

Subscribe to get all the news, info and tutorials you need to build better business apps and sites

Loading animation