Insights and discoveries
from deep in the weeds
Outsharked

Tuesday, August 2, 2011

CsQuery: New features, tests, utility functions

This is an update to my announcement of the CsQuery project, a C# port of jQuery.

In the last few weeks I have added a lot more methods, features, tests, and have made substantial changes to the DOM model to more accurately mimic the HTML DOM objects. The project now includes an Nunit test suite that incorporates some tests migrated from the real jQuery test suite (as applicable) - though there are hundreds and hundreds and it will take time to move them all. So far I've gotten through all of Core, and part of Attribute and Traversal. There are a lot more to go. But I have been heartened by the fact that most migrated tests have passed; so far, most of the problems have had to do with unimplemented features rather than bugs.

Additionally, Styles are represented with a new CSSStyleDeclaration object that includes validation using the Visual Studio CSS validation XML data. Right now I just included a basic CSS3 implementation but this could easily be updated to permit selecting any validation scheme.

With the new Style mechanism, styles added programatically are validated against the schema, and errors are thrown if invalid styles are added. This checking can, of course, be skipped, if desired. It is also not performed while parsing HTML, since this would serve little purpose and probably fail a lot. But this also lets us ensure unit types are parsed and stored correctly in your own code, and that only valid enumeration values are used.

Finally, a number of utility functions and features have been added.


WebForms UpdatePanels can be parsed, manipulated, and re-rendered. The Server.CsQueryHttpContext object now contains an enumerable of AsyncPostbackData objects, which contains a CsQuery object of the data block as well as the identifying information for each UpdatePanel that is updated on an asynchronous postback. You can manipulate the data as usual, and the Render method will take care of reformattingso the ASP.NET client architecture can deal with it properly.

CsQuery.ParseJson - converts JSON to an ExpandoObject. Many methods that accept object in jQuery have been implemented to accept an object in CsQuery. Because it's not convenient to create true objects for every situation where an object structure is being used to pass data or parameters, as in jQuery, we implement this functionality using JSON and ExpanoObjects. For example:

dynamic css = CsQuery.ParseJSON("{ width: 10, height: 10, padding: '1px 10px 1px 10px'}");
CsQuery.Select("div.someClass").CssSet(css);
Methods that are designed to accept an object in jQuery have generally been implemnted in CsQuery to accept an object or an ExpandoObject, but for convenience, they can also accept just a string of JSON data, which will be parsed. Any string that begins with a curly brace will be treated as JSON data in these situations. If a string that is not JSON data but begins with a curly brace is required to be passed, use two curly braces to start the string "{{" to escape.

CsQuery.Extend - mimics jQuery.Extend using ExpandoObjects. You can pass it any legitimate objects (e.g. things with properties, not value types) as parameters, and it will return an ExpandoObject, merging same-named properties of each source object. If you pass a regular object as the target, it will only merge properties that have matches in the source object, since regular objects cannot be extended. Finally, you can pass JSON data directly, and it will be treated as an object and merged.

Examples:

class Person
{
    public string name;
    public float height;
    public float weight;
}
Person person = new Person();
person.height=60;
person.weight=170;

// or using anonymous classes

var person = new { name="", height=60, weight=170 };

// you can use @strings to improve readability, if you like
 
dynamic props = CsQuery.ParseJSON(@"{ 
                                      height: 72.5, 
                                      hairColor: 'brown'
                                    }");

// Merge data into an existing regular object

CsQuery.Extend(person,props);   // => person.height = 72.5
                                //    person.weight=170
                                //    hairColor is ignored since it couldn't be
                                //    added the the target

// Create a new ExpandoObject from existing objects

dynamic fullPerson = CsQuery.Extend(null,person, "{eyeColor: 'blue', noseType: 'ski-jump')");
                               // => fullPerson.height = 72.5
                               //    fullPerson.weight = 170
                               //    fullPerson.eyeColor="blue"
                               //    fullPerson.noseType="ski-jump"

// Extend an existing ExpandoObject

CsQuery.Extend(fullPerson,"{hairColor: 'brown'}");
                               // => fullPerson.height = 72.5
                               //    fullPerson.weight = 170
                               //    fullPerson.eyeColor="blue"
                               //    fullPerson.noseType="ski-jump"
                               //    fullPerson.hairColor="brown"

You can pass "null" as the first parameter (instead of an existing object), in which case an ExpandoObject containing the results of all the sources merged in order will be returned. Parameters may also be enumerable, in which case each member will be iterated and added to the list for processing.

Helper Methods - It's become clear that working with ExpandoObjects and JSON strings a lot is useful when passing data back and forth between client and server. It's also fundamental to how jQuery works. So in addition to adding support for JSON strings natively on methods where appropriate, and so I've added some extension methods to help with this, and other "javascript/C#" conversion issues:

public static bool IsJson(this string text)
True of the string appears to be JSON data (starts with "{" but not "{{")
public static bool IsTruthy(this object obj)
Resolves objects to "true" or "false" based on the Javascript equivalent truthyness of the data type and value.
public static object Clone(this object obj)
public static object Clone(this object obj, bool deep)
Returns a clone of any type of object, simplifying data structures to basic types. If the object is a value type, the value is returned. If it's enumerable, a List<object> is returned. If it's an object or an ExpandoObject, a new ExpandoObject is returned. The "deep" argument indicates that the values of each property should also be cloned.
public static ExpandoObject ToExpando(this object source)
public static ExpandoObject ToExpando(this object source, bool deep)
Converts a regular object to an ExpandoObject, or returns the original object if it's already an ExpandoObject (and deep=false)
public static string ToJSON(this object objectToSerialize)
public static T FromJSON(this string objectToDeserialize)
Convert to & from JSON. This is used directly by CsQuery.ParseJSON and CsQuery.ToJSON, but can also be used as extension methods of string and object. Many more methods and selector improvements added as a result of working through a lot of the jQuery tests. More to come...

No comments:

Post a Comment