My Development Overview of Kentico 9 MVC

When Kentico offered the option to build websites using MVC, I was one of the many developers who jumped at the chance to utilise the new programming model. I've been building websites using the MVC programming model ever since it was first made available in Kentico 7 and with each version, the MVC implementation just got better and better. So much so, I even built my very own website (currently in Kentico 8) in MVC.

MVC in Kentico has always been a bit of a hybrid being in the sense that it wasn't true MVC to the core, which is to be expected when you have to accommodate vast array of features the Kentico platform offers. Luckily for us Kentico 9 has wholeheartedly embraced MVC with open arms and things can only get better with subsequent versions.

I have listed a few observations I thought would be good to write about from my initial experience of using MVC in Kentico 9 whilst working on a client project. I will be talking (very high level) about the changes from previous versions of Kentico MVC as well as the new development approaches for Kentico 9.

1) Goodbye "Pages", Hello "Content-only Pages" Page Types

"Content-only pages" is a new addition to Kentico 9 and is another form of Page Type, with its primary job (as the name suggests) to store content. The main difference between "Pages" and "Content-only Pages", is:

  • Aren't based on Page Templates.
  • Provides a simplified interface when managing content.
  • Does not have a presentation URL. URL patterns now need to be specified which handles the presentation to the MVC site via routing.
  • Lacks the ability to create many Page Aliases.

"Content-only pages" is a requirement to developing MVC sites in Kentico 9. Overall, I actually found "Content-only pages" quite restrictive and useful key page properties are no longer available, such as the URLs and Navigation tabs. I really do wish that these features were left in.

Kentico 9 MVC Missing Page Properties

I will be talking more about the removal of the URLs in my next point, the missing Navigation property is easier to get around. I created a base content page called "Kentico Core Content" that contained all the fields that you would normally find under Navigation properties and inherited this page type on all my content-only pages, such as Articles. You'll then have to just make the customisations to inherit these fields at code level. Easy!

Kentico 9 Core Content Page Inheritance

2) No Document Aliases

There's no option to allow the site administrator to add multiple document aliases for a page. This alone was nearly a deal breaker for me and was tempted to either go down Portal or ASPX templates route. The ability to create multiple document aliases in the URLs section is very powerful feature, especially if you plan on adding 301 redirects.

To get around this excluded feature, you will either have to use URL Rewriting at web.config level or add additional routes at controller level to carry out all specific page redirects.

So before deciding whether to choose the MVC approach, ask yourself if this is pressing feature for you and your client.

3) Separation of the CMS and Presentation Layer

Kentico 8 stepped up the MVC integration by allowing the developer to build their sites using MVC through the CMSApp_MVC project. This created a basic form of separation at project level that was much better suited compared to mixing key MVC components (Controllers/Models/Views) into what is a Web Form powered site in it's infancy in Kentico 7.

Now there is complete separation between the CMS Admin and Presentation layer (or MVC site). Since the CMSApp_MVC approach has been made obselete in Kentico 9, you now have the full ability to create an MVC site as you would do normally in a non-Kentico web application. The only way Kentico and your MVC website can talk to one another is through a Web Farm configuration.

Kentico 9 MVC Architecture

I personally love this setup. My website can be kept light as possible and still harness the power of what Kentico has to offer through using conventional API calls from the Kentico library. I can tell you this for sure, the website itself performs better than ever and no one can tell what CMS is powering the site. Good for security.

4) Licensing and Environment Setup

Due to the need for Web Farm setup to allow syncronisation of content between the CMS and MVC site, the licensing requirements have changed. Based upon how you want to setup your separate sites, Kentico provides different licensing approaches, which pretty much covers all scenarios.

My preferred setup is to run the Kentico and the MVC application under two different sites, on separate domains. Again, my reasoning comes down to catering for that additional level of security where the management of your site is on a sub-domain and not so obvious where the administration area resides. In this case, two licenses will be required. For example:

  • MVC Website: www.surinderbhomra.com
  • Kentico Admin: admin.surinderbhomra.com

You will get a free license for the Kentico site as long as the subdomain is "admin".

The only sad part (for me personally) is that Kentico CMS Free Edition license does not allow for MVC websites. I really do hope that this changes at some point. I'd love to utilise full Kentico 9 MVC on future personal projects that are currently on the free edition. Otherwise they will forever be stuck in version 8.2.

5) Page Templates

The ability to use Page Templates alongside standard page types is still available within the Kentico interface, but you can only develop an MVC site this way by creating a (now obsolete) "CMSApp_MVC" project. Kentico 9 MVC is still fully backwards compatible with this approach.

6) Retrieving Content In MVC Controllers

In Kentico 8, a controller acted as the code-behind to your Page Template where you could get all the information about a current page by calling DocumentContext.CurrentDocument. In Kentico 9, this is no longer the case and it is recommended content should be retrieved using its auto-generated code. I generally don't go down the route of using the auto-generated code. I instead like to create my own custom methods so I have the freedom to pull out the exact data my data needs by passing the Node Alias Path into my control from the URL route patten. Personal preference.

7) Friendly URL's Should Include A Node ID

Kentico recommends all page URL's should consist of NodeID and Page alias, to ensure optimum search engine optimisation on the event the page alias of pages changes on the live site. Kentico's documentation states:

Typically, a page alias will be part of a macro expression that makes up a URL pattern in content only page types. For example, a URL pattern can be specified like this /Articles/{%NodeID%}/{%NodeAlias%}. Where {%NodeAlias%} accesses the page alias.

I've gone down the route of creating a custom route contraint in my MVC project, to allow me to retrieve TreeNode for the current document via HttpContext just from passing the Node Alias only. I could go into more detail, but this is probably best suited for another blog post.

Conclusion

The MVC integration in Kentico is like a fine wine. It gets better and better every year (in this case release) and they should be congratulated for undertaking such a humongous task. Would I choose it over Portal or ASPX pages for every project? Probably not, because I can see clients expecting functionality that is not quite present in an MVC installation as of yet.

I do like the freedom MVC gives me whilst harnessing the power of Kentico and it works great on those projects that require maximum flexibility and the seperation of code-levels allows me to do that. In addition, if my site requires scaling, I can easily move it to Azure. I am very much looking forward to what Kentico has in store for feature releases.

If there is anything I have listed in my initial observation that are incorrect, please leave a comment and I will update this post.

Useful Method To Deserialize XML or JSON To A Class Object

I have created a helper class that will allow me to consume any XML or JSON request for deserialization into a class object. As you can see from the code below, the GetJsonRequest() and GetXmlRequest() methods allow you to pass an unknown type as well as the URL to where you are getting your request from. This makes things very straight-forward when you want to easily strongly type the data.

public class ApiWebRequestHelper
{
    /// <summary>
    /// Gets a request from an external JSON formatted API and returns a deserialized object of data.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="requestUrl"></param>
    /// <returns></returns>
    public static T GetJsonRequest<T>(string requestUrl)
    {
        try
        {
            WebRequest apiRequest = WebRequest.Create(requestUrl);
            HttpWebResponse apiResponse = (HttpWebResponse)apiRequest.GetResponse();

            if (apiResponse.StatusCode == HttpStatusCode.OK)
            {
                string jsonOutput;
                using (StreamReader sr = new StreamReader(apiResponse.GetResponseStream()))
                    jsonOutput = sr.ReadToEnd();
                    
                var jsResult = JsonConvert.DeserializeObject<T>(jsonOutput);

                if (jsResult != null)
                    return jsResult;
                else
                    return default(T);
            }
            else
            {
                return default(T);
            }
        }
        catch (Exception ex)
        {
            // Log error here.

            return default(T);
        }
    }

    /// <summary>
    /// Gets a request from an external XML formatted API and returns a deserialized object of data.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="requestUrl"></param>
    /// <returns></returns>
    public static T GetXmlRequest<T>(string requestUrl)
    {
        try
        {
            WebRequest apiRequest = WebRequest.Create(requestUrl);
            HttpWebResponse apiResponse = (HttpWebResponse)apiRequest.GetResponse();

            if (apiResponse.StatusCode == HttpStatusCode.OK)
            {
                string xmlOutput;
                using (StreamReader sr = new StreamReader(apiResponse.GetResponseStream()))
                    xmlOutput = sr.ReadToEnd();

                XmlSerializer xmlSerialize = new XmlSerializer(typeof(T));

                var xmlResult = (T)xmlSerialize.Deserialize(new StringReader(xmlOutput));

                if (xmlResult != null)
                    return xmlResult;
                else
                    return default(T);
            }
            else
            {
                return default(T);
            }
        }
        catch (Exception ex)
        {
            // Log error here.
            return default(T);
        }
    }
}

The ApiWebRequestHelper class relies on the following namespaces:

  • Newtonsoft Json
  • System.Xml.Serialization
  • ​​System.IO;

The ApiWebRequestHelper can be used in the following way:

// Get Json Request
ApiWebRequestHelper.GetJsonRequest<MyCustomJsonClass>("http://www.surinderbhomra.com/api/result.json");

// Get XML Request
ApiWebRequestHelper.GetXmlRequest<MyCustomXMLClass>("http://www.surinderbhomra.com/api/result.xml");

Resize An Instagram Image Using A Media Query Parameter

This is something I have been meaning to post for quite some time, ever since I first started working on integrating Instagram's API in web applications from 2013. - The ability to resize an image from Instagram without having to deal with registering for an API key and worrying about request limits.

This approach is ideal if you have no requirement to get further information about an image, such as description, comments, likes etc.

Believe it or not, Instagram contains quite a neat (somewhat hidden) feature that gives you the ability to output three different sized images directly into a webpage, by constructing the path to an image as so: *https://www.instagram.com/p/<image-id>**/media/?size=<size-parameter>***.

The supported "size parameters" are:

  • t - for thumbnail (150px x 150px)
  • m - for medium (306px x 306px)
  • l - for large (640px x 640px)

The great thing about using the media parameter is that the requested image size is served up immediately. For example, we could embed an Instagram image directly into our HTML markup as so:

<p style="text-align: center;">
  <img alt="Mr. Brown - The Office Dog" src="https://www.instagram.com/p/uz_8x2qW6E/media/?size=m">
</p>

Which will render the following output:

Mr. Brown - The Office Dog

In this case, this is a picture of Mr. Brown (the office dog) from my Instagram profile in medium size.

JSON/XML To Strongly-Typed C# Object In Visual Studio

Today, I stumbled across a really neat feature in Visual Studio 2015 that gives you the ability to create a strongly-typed C# class directly into your class library. I'm amazed that I've happened to overlook this key feature since Visual Studio 2012!

Better late than never.

In the past, when consuming large JSON data-structures, I normally head off to http://json2csharp.com to help me get the initial class objects generated, which works a treat. I've blogged about my experiences using json2csharp here.

The strongly-type class generator feature is hidden away in a place I would never have thought to look - Edit > Paste Special, where you will be given the option to either generate a class object based on XML or JSON. But it's in there for a reason.

Paste Special - To Json or XML Class

All that needs to be done now is to either copy some XML or JSON ready to paste into a newly created class. I do find the generated output quite untidy, but this is a great starting point to generating complex data structures.

Is ReactJS A Worthwhile Addition To An ASP.NET MVC Application?

ReactJSI've been meddling around with ReactJS over the last week or so, seeing if this is something viable to use for future client projects. I am always constantly on the lookout to whether there are better alternatives on how my fellow developers and I develop our sites.

Throughout the sample applications I've been building, I constantly asked myself one question: Why Would I Use ReactJS In My Day To Day Development? I am ASP.NET developer who build websites either using Web Forms or MVC Razor. So I am finding it difficult to comprehend whether using ReactJS is viable in these frameworks, especially MVC.

ReactJS is primarily a view framework where you have the ability to write component-based web applications directly into your JavaScript that then gets output to the DOM virtually - making for a very fast and responsive webpage. It's a different approach to developing websites that I quite like. But for the moment, I just don't see how it can benefit me when the full MVC framework does a pretty good job with all the bells and whistles.

For example, I segregate all my key HTML markup into partial views in order to increase re-use throughout my web application, which works really well when making AJAX calls where the markup needs to be displayed on the page asynchronously as well as server-side. I can just see by implementing ReactJS, I will be duplicating this process at JavaScript and CSHTML level if a markup change ever needed to be made. If partial views does the job effectively, I'm not too sure the need for ReactJS in my future ASP.NET MVC creations.

Don't get me wrong - I really like ReactJS. It makes writing JavaScript an even more enjoyable experience purely due to the JSX syntax. Long gone are the days where you have to concatenate strings to form HTML. More importantly, it's readable and truly scalable.

Unfortunately, it doesn't look like ReactJS is a viable option for me at this moment in time. I can see how it would be a very useful framework for building web applications where there is a requirement for the view to be created strictly client-side along with heavy use of AJAX calls from an API layer to serve data to your application. But in situations where you have two frameworks that provide the ability to create views, in this case ReactJS and ASP.NET MVC, it doesn't make sense.

I could be talking absolute nonsense and missing the whole point. If this is the case (most likely!), please leave a comment.

Securely Erasing An Android Device

I decided it was time for my trusted Nexus 5 and I to part ways. We had many good times over the last few years we had known each other. But as things sometimes unfortunately turn out, our time together finally came to an end. The battery life was starting to wane and I really need a phone with a larger capacity - more than 16GB.

Enough with the sentimentality...

I am now the proud owner of a Nexus 6P.

My Nexus 6P

And what a beauty she is!

In getting the Nexus 6P, I am about to sell my dear Nexus 5 on eBay, so that it may grace someone elses life in a positive way just as it had done mine. It's in very good condition and still looks brand-spanking new. The only thing that I need to ensure is that the phone is wiped clean - inside and out. Even though I carried out a factory reset, I read doing this alone is not enough to make your previously stored information fully un-recoverable.

To be completely sure that the device has had a complete wipe down, you need to ensure the Android device is encrypted prior to carrying out the factory reset. You can check if the phone is set to be encrpyted by going into Settings > Security > Encryption. By doing this, the encryption process will scramble your data and if some data is left after the factory reset, a key would be required to unecrypt it. For the general user, this should suffice.

I decided to take things a step further, just to be 100% sure. I found a very good post on StackExchange Security that states the following:

Factory resets reset your phone to a stock like state but does not remove your data, just applications. This leaves some data behind. The best way to prevent this data from being recovered is to encrypt the phone, and use wipe data/factory reset from the recovery menu. This way you don't have to download a ton of data and you can be fairly certain your things are secure.


If you're not that worried, encrypting, and data/factory reset from the recovery menu is probably enough.


Forgot to mention why the recovery mode data/factory reset. It formats the user data areas as well as the application area, and is far more thorough/secure than the one inside of the android OS and will prevent you from download junk data. It just flat out gets rid of it.


To get to the recovery console on the Nexus 5 for resetting:

  1. If your phone is on, turn it off.
  2. Press and hold the Volume Down and Volume Up buttons, and then press and hold the Power button at the same time until the phone turns on. You'll see the word "Start" with an arrow around it.
  3. Press the Volume Down button twice to highlight "Recovery."
  4. Press the Power button to start Recovery mode. You'll see an image of an Android robot with a red exclamation mark and the words "No command."
  5. While holding the Power button, press and release the Volume Up button once.
  6. Press the Volume Down button twice to highlight "wipe data/factory reset" and press the Power button to select it.
  7. Press the Volume Down button seven times to highlight "Yes - erase all user data" and press the Power button to select it.
  8. After the reset is complete, press the Power button to select the option to reboot your device.

Don't forget to also revoke account access as an extra measure by logging into your Google Account and clicking the "Remove" button (https://security.google.com/settings/security/activity).

Disconnecting Nexus 5 from Google Account

I am Just One Of Those Developers...

...who hasn’t created a new open-source plugin/library, answered many posts on StackOverflow (as much as I’d like to!), made an active contribution to Github, created a Pluralsight course, or coded something beautiful on CodePen.

How very selfish of me.

But what I do know is that this doesn’t make me any less of a developer. I have the capability to translate something in it’s infancy to truly something awesome that I am confident I will be very much proud of. I think as a developer that's quite easy to lose sight of - I know I feel that way. Just throw a problem or project my way and I’ll do it.

Would I like to have the capability to everything I stated in my first sentence? Yes! Who wouldn’t? I look at my experienced peers (to whom I refer to as “the greats”) in pure admiration and hoping one day I will have the capacity to contribute to the programming world as they do.

As I gaze back at my 8 years in the programming world, one thought comes to mind: I should be doing more. Thoughts like these is was what separates us from just being good at what we do to something much much more.

Kentico Contact Management: Update Visitor Information In Code

Over the last few months, I've been working away on a Kentico 9 EMS project. Very exciting indeed! Really pulling out all the stops on trying to make an amazing site in an EMS framework alongside the whole team at Syndicut.

This week, I had to develop a custom built form to grab some user's information. Unfortunately, I was unable to use Kentico's Form Builder due to the requirement where the details had to be submitted to an external API, as well as utilising a custom design.

One of the many benefits you get out-the-box from using the Kentico Form Builder is the ability to log marketing activity to the Contact Management area to store information on the visitors who view and interact with your site. By building a custom form, I was losing losing key data, such as: First Name, Last Name Email Address and Telephone No. - The base key criteria of turning an anonymous visitor to a person with some context.

To avoid this loss of data, I created a method that uses the CMS.OnlineMarketingContext class to allow you to manually update the current visitor information:

/// <summary>
/// Creates/Updates a contact in Kentico's Contact Management system.
/// </summary>
/// <param name="firstName">Visitors first name</param>
/// <param name="lastName">Visitors last name</param>
/// <param name="emailAddress">Visitors email address</param>
/// <param name="businessPhone">Visitors business phone number</param>
/// <param name="allowContactOverwrite">Overwrite existing contact fields with new information.</param>
public static void SetMarketingContact(string firstName, string lastName, string emailAddress, string businessPhone, bool allowContactOverwrite = false)
{
    ContactInfo existingContact = OnlineMarketingContext.GetCurrentContact();
            
    // Update an existing contact or create a new one if no existing contact exists.
    if (existingContact != null)
    {
        existingContact.ContactFirstName = firstName;
        existingContact.ContactLastName = lastName;
        existingContact.ContactEmail = emailAddress;
        existingContact.ContactBusinessPhone = businessPhone;

        if (allowContactOverwrite)
            existingContact.Update();
    }
}

My code only shows a handful of fields that I am updating, but the "ContactInfo" class provides access to many more.

Extending Kentico DataHelper GetDataRowValue() and GetDataRowViewValue()

After a long blogging hiatus, I decided to make my somewhat grand return! So here we go...

There are times when you need to retrieve documents from using the TreeProvider.SelectNodes() that can return DataRowViews or DataRows of node information. Now, Kentico provides "DataHelper.GetDataRowViewValue()" and "DataHelper.DataRowValue()" methods to output your required fields of information. But I find these DataHelper methods quite tedious when you have a massive collection of node data. I am lazy and would do anything to make my life easier.

So I created a DataHelperExtension class object that would allow me to get my commonly used DataRow and DataRowView fields with ease:

  • Document ID
  • Class Name
  • Node Alias Path
  • Get String Value (for custom fields)
  • Get Integer Value (for custom fields)
  • Page Menu Name
  • Node Has Children
public static class DataHelperExtensions
{
    /// <summary>
    /// Get the Document ID.
    /// </summary>
    /// <param name="dr"></param>
    /// <returns></returns>
    public static int DocumentId(this DataRow dr)
    {
        return int.Parse(DataHelper.GetDataRowValue(dr, "DocumentID").ToString());
    }

    /// <summary>
    /// Get the document Class Name.
    /// </summary>
    /// <param name="dr"></param>
    /// <returns></returns>
    public static string ClassName(this DataRow dr)
    {
        return DataHelper.GetDataRowValue(dr, "ClassName").ToString();
    }
        
    /// <summary>
    /// Get Node Alias Path.
    /// </summary>
    /// <param name="dr"></param>
    /// <param name="fullUrlPath"></param>
    /// <returns></returns>
    public static string NodeAliasPath(this DataRow dr, bool fullUrlPath = true)
    {
        if (!fullUrlPath)
            return DataHelper.GetDataRowValue(dr, "NodeAliasPath").ToString();
        else
            return DocumentURLProvider.GetUrl(DataHelper.GetDataRowValue(dr, "NodeAliasPath").ToString());
    }

    /// <summary>
    /// Get custom string field.
    /// </summary>
    /// <param name="dr"></param>
    /// <param name="fieldName"></param>
    /// <returns></returns>
    public static string GetStringValue(this DataRow dr, string fieldName)
    {
        if (DataHelper.GetDataRowValue(dr, fieldName) != null)
            return DataHelper.GetDataRowValue(dr, fieldName).ToString();
        else
            return String.Empty;
    }
        
    /// <summary>
    /// Get custom integer field.
    /// </summary>
    /// <param name="dr"></param>
    /// <param name="fieldName"></param>
    /// <returns></returns>
    public static int GetIntegerValue(this DataRow dr, string fieldName)
    {
        if (DataHelper.GetDataRowValue(dr, fieldName) != null)
            return int.Parse(DataHelper.GetDataRowValue(dr, fieldName).ToString());
        else
            return 0;
    }
        
    /// <summary>
    /// Get Menu Caption of a page, otherwise default to the Document Name.
    /// </summary>
    /// <param name="dr"></param>
    /// <returns></returns>
    public static string PageMenuName(this DataRow dr)
    {
        string menuCaption = DataHelper.GetDataRowValue(dr, "DocumentMenuCaption").ToString();

        if (String.IsNullOrEmpty(menuCaption))
            menuCaption = DataHelper.GetDataRowValue(dr, "DocumentName").ToString();

        return menuCaption;
    }

    /// <summary>
    /// Check if node has children.
    /// </summary>
    /// <param name="dr"></param>
    /// <returns></returns>
    public static bool NodeHasChildren(this DataRow dr)
    {
        return bool.Parse(DataHelper.GetDataRowValue(dr, "NodeHasChildren").ToString());
    }

    /// <summary>
    /// Get the Document ID.
    /// </summary>
    /// <param name="drv"></param>
    /// <returns></returns>
    public static int DocumentId(this DataRowView drv)
    {
        return int.Parse(DataHelper.GetDataRowViewValue(drv, "DocumentID").ToString());
    }

    /// <summary>
    /// Get the document Class Name.
    /// </summary>
    /// <param name="drv"></param>
    /// <returns></returns>
    public static string ClassName(this DataRowView drv)
    {
        return DataHelper.GetDataRowViewValue(drv, "ClassName").ToString();
    }

    /// <summary>
    /// Get Node Alias Path.
    /// </summary>
    /// <param name="drv"></param>
    /// <param name="fullUrlPath"></param>
    /// <returns></returns>
    public static string NodeAliasPath(this DataRowView drv, bool fullUrlPath = true)
    {
        if (!fullUrlPath)
            return DataHelper.GetDataRowViewValue(drv, "NodeAliasPath").ToString();
        else
            return DocumentURLProvider.GetUrl(DataHelper.GetDataRowViewValue(drv, "NodeAliasPath").ToString());
    }

    /// <summary>
    /// Get custom string field.
    /// </summary>
    /// <param name="drv"></param>
    /// <param name="fieldName"></param>
    /// <returns></returns>
    public static string GetStringValue(this DataRowView drv, string fieldName)
    {
        if (DataHelper.GetDataRowViewValue(drv, fieldName) != null)
            return DataHelper.GetDataRowViewValue(drv, fieldName).ToString();
        else
            return String.Empty;
    }

    /// <summary>
    /// Get custom integer field.
    /// </summary>
    /// <param name="drv"></param>
    /// <param name="fieldName"></param>
    /// <returns></returns>
    public static int GetIntegerValue(this DataRowView drv, string fieldName)
    {
        if (DataHelper.GetDataRowViewValue(drv, fieldName) != null)
            return int.Parse(DataHelper.GetDataRowViewValue(drv, fieldName).ToString());
        else
            return 0;
    }

    /// <summary>
    /// Get Menu Caption of a page, otherwise default to the Document Name.
    /// </summary>
    /// <param name="drv"></param>
    /// <returns></returns>
    public static string PageMenuName(this DataRowView drv)
    {
        string menuCaption = DataHelper.GetDataRowViewValue(drv, "DocumentMenuCaption").ToString();

        if (String.IsNullOrEmpty(menuCaption))
            menuCaption = DataHelper.GetDataRowViewValue(drv, "DocumentName").ToString();

        return menuCaption;
    }

    /// <summary>
    /// Check if node has children.
    /// </summary>
    /// <param name="drv"></param>
    /// <returns></returns>
    public static bool NodeHasChildren(this DataRowView drv)
    {
        return bool.Parse(DataHelper.GetDataRowViewValue(drv, "NodeHasChildren").ToString());
    }
}

Here's an example on how to put this DataHelperExtension class into use:

if (!DataHelper.DataSourceIsEmpty(data))
{
    DataView sortedData = data.Tables[0].DefaultView;

    foreach (DataRowView drvTestimonial in sortedData)
    {
        int docId = drvTestimonial.DocumentId(); // Get the Document ID
        string title = drvTestimonial.GetStringValue("Title"); // Custom field

        // Do something with the fields of data.
    }
}

Simple and effective!