The Immutable Collections package is awesome.  If you’re writing concurrent code, immutable collections should be your new best friends (along with immutable classes in general).  Explicit locking is bad, bad, bad, with an extra helping of bad (repeat this until it sticks).

A typical pattern you’ll see when modifying a collection looks like this:

myCollection = myCollection.Add( "Banana" );

However if the “myCollection” above is a field you’re sharing between threads, you still need to protect it.  This is easy with the System.Threading.Interlocked helpers:

Interlocked.Exchange(
    ref myCollection,
    myCollection.Add( "Banana" ) );

But what if you’re updating an ImmutableDictionary and need an atomic update, so it’ll only add an item if it doesn’t exist?  Here’s where the ImmutableInterlocked helpers come in:

private ImmutableDictionary<string, Fruit> myDictionary
    = ImmutableDictionary<string, Fruit>.Empty;
…
var fruit = ImmutableInterlocked.GetOrAdd(
    ref myDictionary,
    "banana",
    new Banana() );

Now things could get interesting.  You’ll notice on the line above we create a new Banana instance.  If “banana” already exists in the dictionary, the nice fresh Banana we created will just be discarded.  In many cases this isn’t a problem (maybe a slip hazard); it’s just a redundant object creation.

But what if it’s something we only want to create once, and only if it doesn’t exist?  ImmutableInterlocked has a GetOrAdd override that takes a delegate:

var fruit = ImmutableInterlocked.GetOrAdd(
    ref myDictionary,
    "banana",
    _ => new Banana() );

It sure looks promising.  Presumably it only calls the delegate if the item isn’t in the dictionary?…  Nope!  Apparently it always calls the delegate, checks if the item exists, and discards the result if it does (while the source code isn’t currently available, we can get a vague idea how it might be implemented from this Unofficial port of Immutable Collections).

So it seems we need another solution.  We really don’t want to explicitly lock anything (bad, bad, bad).  Turns out we can get this for “free” if we use Lazy<T>:

private ImmutableDictionary<string, Lazy<Fruit>> myDictionary
    = ImmutableDictionary<string, Lazy<Fruit>>.Empty;
…
var fruit = ImmutableInterlocked.GetOrAdd(
    ref myDictionary,
    "banana",
    new Lazy<Fruit>( () => new Banana(), true ) ).Value;

This ensures there’s a Lazy<Fruit> in the dictionary that knows how to create a Banana on demand.  Lazy<T> already takes care of ensuring only one thread can actually create the instance.  It does some internal locking of its own, but apparently it’s super efficient so we can happily ignore it and go on our way.

Hope this helps!

Check this out – http://referencesource-beta.microsoft.com/

Using the new and cool Roslyn “Compiler as a Service” as [a kind of] online Reflector / DotPeek / ILSpy:)

Following my previous post about implementing an AVL tree in Objective-C, here’s an ImmutableArray that makes use of it:

ImmutableArray gist

It’s very loosely based on .NET & Mono’s ImmutableList<T>, but follows conventions similar to NSMutableArray (the main difference is most methods return a new ImmutableArray).

The main overhead compared to a regular NSMutableArray is from the extra object allocations and deletions it may need.  If you need to add many objects quickly, retaining a cache of “old” trees and then releasing it afterwards (perhaps asynchronously on a dispatch queue) will squeeze out a little more performance.  Here’s a simple example:

__block NSMutableArray *cache = [NSMutableArray new];
ImmutableList *myList = [ImmutableList empty];

for ( int n = 0; n < 10000; ++ n )
{
    myList = [myList addObject:@( n )];
    [cache addObject:myList];
}

dispatch_async(
    dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0 ),
    ^{ cache = nil; } );

Note this will use a little more memory (as old tree nodes will be retained by the cache).

Finally, here are some rough timings (adding 20000 NSNunbers):

AVL tree (mutable)
2014-01-18 00:24:09.075 AVLTree[99465:303] elapsed subtime = 27.864
2014-01-18 00:24:09.109 AVLTree[99465:303] elapsed subtime = 30.158
2014-01-18 00:24:09.140 AVLTree[99465:303] elapsed subtime = 29.922
2014-01-18 00:24:09.173 AVLTree[99465:303] elapsed subtime = 32.26
2014-01-18 00:24:09.209 AVLTree[99465:303] elapsed subtime = 35.307
2014-01-18 00:24:09.245 AVLTree[99465:303] elapsed subtime = 36.04
2014-01-18 00:24:09.282 AVLTree[99465:303] elapsed subtime = 34.085
2014-01-18 00:24:09.314 AVLTree[99465:303] elapsed subtime = 31.594
2014-01-18 00:24:09.349 AVLTree[99465:303] elapsed subtime = 32.881
2014-01-18 00:24:09.439 AVLTree[99465:303] elapsed time = 391.271

AVL node (immutable)
2014-01-18 00:24:09.540 AVLTree[99465:303] elapsed subtime = 100.848
2014-01-18 00:24:09.617 AVLTree[99465:303] elapsed subtime = 75.399
2014-01-18 00:24:09.694 AVLTree[99465:303] elapsed subtime = 76.56
2014-01-18 00:24:09.771 AVLTree[99465:303] elapsed subtime = 76.792
2014-01-18 00:24:09.857 AVLTree[99465:303] elapsed subtime = 84.679
2014-01-18 00:24:09.940 AVLTree[99465:303] elapsed subtime = 82.133
2014-01-18 00:24:10.021 AVLTree[99465:303] elapsed subtime = 81.026
2014-01-18 00:24:10.105 AVLTree[99465:303] elapsed subtime = 82.746
2014-01-18 00:24:10.197 AVLTree[99465:303] elapsed subtime = 91.938
2014-01-18 00:24:10.287 AVLTree[99465:303] elapsed time = 847.492

ImmutableArray
2014-01-18 00:24:10.363 AVLTree[99465:303] elapsed subtime = 64.775
2014-01-18 00:24:10.447 AVLTree[99465:303] elapsed subtime = 83.325
2014-01-18 00:24:10.526 AVLTree[99465:303] elapsed subtime = 77.767
2014-01-18 00:24:10.606 AVLTree[99465:303] elapsed subtime = 80.301
2014-01-18 00:24:10.718 AVLTree[99465:303] elapsed subtime = 100.795
2014-01-18 00:24:10.808 AVLTree[99465:303] elapsed subtime = 88.625
2014-01-18 00:24:10.897 AVLTree[99465:303] elapsed subtime = 88.9
2014-01-18 00:24:10.988 AVLTree[99465:303] elapsed subtime = 90.065
2014-01-18 00:24:11.076 AVLTree[99465:303] elapsed subtime = 87.954
2014-01-18 00:24:11.169 AVLTree[99465:303] elapsed time = 870.628

ImmutableArray with cache (deferred release)
2014-01-18 01:21:42.137 AVLTree[99599:303] elapsed subtime = 60.761
2014-01-18 01:21:42.210 AVLTree[99599:303] elapsed subtime = 71.828
2014-01-18 01:21:42.293 AVLTree[99599:303] elapsed subtime = 82.437
2014-01-18 01:21:42.379 AVLTree[99599:303] elapsed subtime = 84.563
2014-01-18 01:21:42.470 AVLTree[99599:303] elapsed subtime = 91.247
2014-01-18 01:21:42.557 AVLTree[99599:303] elapsed subtime = 85.93
2014-01-18 01:21:42.630 AVLTree[99599:303] elapsed subtime = 72.06
2014-01-18 01:21:42.706 AVLTree[99599:303] elapsed subtime = 76.108
2014-01-18 01:21:42.788 AVLTree[99599:303] elapsed subtime = 80.562
2014-01-18 01:21:42.873 AVLTree[99599:303] elapsed time = 796.389

NSMutableArray
2014-01-18 00:24:11.180 AVLTree[99465:303] elapsed subtime = 0.176013
2014-01-18 00:24:11.181 AVLTree[99465:303] elapsed subtime = 0.177979
2014-01-18 00:24:11.181 AVLTree[99465:303] elapsed subtime = 0.234008
2014-01-18 00:24:11.182 AVLTree[99465:303] elapsed subtime = 0.231981
2014-01-18 00:24:11.183 AVLTree[99465:303] elapsed subtime = 0.162005
2014-01-18 00:24:11.183 AVLTree[99465:303] elapsed subtime = 0.295997
2014-01-18 00:24:11.184 AVLTree[99465:303] elapsed subtime = 0.153005
2014-01-18 00:24:11.184 AVLTree[99465:303] elapsed subtime = 0.147998
2014-01-18 00:24:11.185 AVLTree[99465:303] elapsed subtime = 0.147998
2014-01-18 00:24:11.186 AVLTree[99465:303] elapsed time = 6.36405

NSMutableArray with locking (@synchronized)
2014-01-18 00:24:11.187 AVLTree[99465:303] elapsed subtime = 0.769973
2014-01-18 00:24:11.188 AVLTree[99465:303] elapsed subtime = 0.718951
2014-01-18 00:24:11.190 AVLTree[99465:303] elapsed subtime = 0.68599
2014-01-18 00:24:11.191 AVLTree[99465:303] elapsed subtime = 0.788987
2014-01-18 00:24:11.192 AVLTree[99465:303] elapsed subtime = 0.699997
2014-01-18 00:24:11.193 AVLTree[99465:303] elapsed subtime = 0.711024
2014-01-18 00:24:11.194 AVLTree[99465:303] elapsed subtime = 0.693977
2014-01-18 00:24:11.195 AVLTree[99465:303] elapsed subtime = 0.721037
2014-01-18 00:24:11.196 AVLTree[99465:303] elapsed subtime = 0.687957
2014-01-18 00:24:11.198 AVLTree[99465:303] elapsed time = 10.95

NSArray (arrayByAddingObject:)
2014-01-18 00:24:11.238 AVLTree[99465:303] elapsed subtime = 39.774
2014-01-18 00:24:11.366 AVLTree[99465:303] elapsed subtime = 127.631
2014-01-18 00:24:11.573 AVLTree[99465:303] elapsed subtime = 205.681
2014-01-18 00:24:11.862 AVLTree[99465:303] elapsed subtime = 288.225
2014-01-18 00:24:12.238 AVLTree[99465:303] elapsed subtime = 375.7
2014-01-18 00:24:12.685 AVLTree[99465:303] elapsed subtime = 446.18
2014-01-18 00:24:13.208 AVLTree[99465:303] elapsed subtime = 522.149
2014-01-18 00:24:13.844 AVLTree[99465:303] elapsed subtime = 635.206
2014-01-18 00:24:14.604 AVLTree[99465:303] elapsed subtime = 759.399
2014-01-18 00:24:15.589 AVLTree[99465:303] elapsed time = 4391.24

If you’re looking for an Objective-C implementation of an immutable AVL tree, based on Mono’s implementation of AvlNode (System.Collections.Immutable namespace), you’ve come to the right place :)

You can find the code right here: AvlNode gist

Insert performance is considerably slower than an NSMutable array, and about half the speed of a mutable AVL tree (due to the extra object allocations). However because this implementation is immutable (any operation on the tree potentially creates a new root node, and doesn’t alter the base tree at all), trees can be shared freely between threads. This has the benefit no locking / synchronization is needed to “modify” the collection.

Another interesting property is you’re free to retain “old” trees, effectively keeping a snapshot of past states.

Here are some timings for comparison (inserting 50000 NSNumbers). Note the increasing times of the regular immutable NSArray, using arrayByAddingObject:

AVL tree (mutable)
2014-01-17 21:44:40.003 AVLTree[98872:303] elapsed subtime = 33.4581
2014-01-17 21:44:40.045 AVLTree[98872:303] elapsed subtime = 37.948
2014-01-17 21:44:40.082 AVLTree[98872:303] elapsed subtime = 36.437
2014-01-17 21:44:40.123 AVLTree[98872:303] elapsed subtime = 40.668
2014-01-17 21:44:40.165 AVLTree[98872:303] elapsed subtime = 40.554
2014-01-17 21:44:40.209 AVLTree[98872:303] elapsed subtime = 43.584
2014-01-17 21:44:40.250 AVLTree[98872:303] elapsed subtime = 40.233
2014-01-17 21:44:40.284 AVLTree[98872:303] elapsed subtime = 33.158
2014-01-17 21:44:40.322 AVLTree[98872:303] elapsed subtime = 37.446
2014-01-17 21:44:40.361 AVLTree[98872:303] elapsed time = 391.786

AVL node (immutable)
2014-01-17 21:44:40.441 AVLTree[98872:303] elapsed subtime = 78.872
2014-01-17 21:44:40.536 AVLTree[98872:303] elapsed subtime = 93.391
2014-01-17 21:44:40.631 AVLTree[98872:303] elapsed subtime = 93.877
2014-01-17 21:44:40.727 AVLTree[98872:303] elapsed subtime = 95.449
2014-01-17 21:44:40.835 AVLTree[98872:303] elapsed subtime = 107.319
2014-01-17 21:44:40.943 AVLTree[98872:303] elapsed subtime = 106.377
2014-01-17 21:44:41.043 AVLTree[98872:303] elapsed subtime = 99.7339
2014-01-17 21:44:41.145 AVLTree[98872:303] elapsed subtime = 100.765
2014-01-17 21:44:41.285 AVLTree[98872:303] elapsed subtime = 140.152
2014-01-17 21:44:41.397 AVLTree[98872:303] elapsed time = 1034.59

Mutable array
2014-01-17 21:44:41.412 AVLTree[98872:303] elapsed subtime = 0.232995
2014-01-17 21:44:41.413 AVLTree[98872:303] elapsed subtime = 0.267982
2014-01-17 21:44:41.414 AVLTree[98872:303] elapsed subtime = 0.232995
2014-01-17 21:44:41.415 AVLTree[98872:303] elapsed subtime = 0.340998
2014-01-17 21:44:41.415 AVLTree[98872:303] elapsed subtime = 0.244975
2014-01-17 21:44:41.416 AVLTree[98872:303] elapsed subtime = 0.337005
2014-01-17 21:44:41.417 AVLTree[98872:303] elapsed subtime = 0.227988
2014-01-17 21:44:41.418 AVLTree[98872:303] elapsed subtime = 0.178993
2014-01-17 21:44:41.419 AVLTree[98872:303] elapsed subtime = 0.231981
2014-01-17 21:44:41.420 AVLTree[98872:303] elapsed time = 8.18902

Mutable array with locking
2014-01-17 21:44:41.422 AVLTree[98872:303] elapsed subtime = 0.946999
2014-01-17 21:44:41.423 AVLTree[98872:303] elapsed subtime = 0.873029
2014-01-17 21:44:41.425 AVLTree[98872:303] elapsed subtime = 0.846028
2014-01-17 21:44:41.426 AVLTree[98872:303] elapsed subtime = 0.813007
2014-01-17 21:44:41.427 AVLTree[98872:303] elapsed subtime = 0.771999
2014-01-17 21:44:41.428 AVLTree[98872:303] elapsed subtime = 0.835001
2014-01-17 21:44:41.430 AVLTree[98872:303] elapsed subtime = 0.808001
2014-01-17 21:44:41.431 AVLTree[98872:303] elapsed subtime = 0.815034
2014-01-17 21:44:41.432 AVLTree[98872:303] elapsed subtime = 0.89401
2014-01-17 21:44:41.434 AVLTree[98872:303] elapsed time = 13.129

Immutable array
2014-01-17 21:44:41.489 AVLTree[98872:303] elapsed subtime = 52.946
2014-01-17 21:44:41.638 AVLTree[98872:303] elapsed subtime = 147.815
2014-01-17 21:44:41.886 AVLTree[98872:303] elapsed subtime = 247.666
2014-01-17 21:44:42.201 AVLTree[98872:303] elapsed subtime = 313.846
2014-01-17 21:44:42.620 AVLTree[98872:303] elapsed subtime = 419.08
2014-01-17 21:44:43.122 AVLTree[98872:303] elapsed subtime = 500.611
2014-01-17 21:44:43.720 AVLTree[98872:303] elapsed subtime = 597.283
2014-01-17 21:44:44.413 AVLTree[98872:303] elapsed subtime = 692.916
2014-01-17 21:44:45.245 AVLTree[98872:303] elapsed subtime = 830.406
2014-01-17 21:44:46.305 AVLTree[98872:303] elapsed time = 4868.87

Face detection has been possible for some time on iOS thanks to libraries like OpenCV.  The CIDetector class introduced in iOS 5 made it a standard feature.  Since iOS 7 it can also detect smiles and eye blinks Smile

With iOS 6, AV Foundation gained AVCaptureMetadataOutput, allowing face detection to be included in the capture pipeline (in iOS 7 it also supports barcode scanning).

Here’s how you could use that to perform face masking on live video:

Blockhead

First thing to do is get the capture session set up:

	AVCaptureSession *captureSession = [AVCaptureSession new];
 
	[captureSession beginConfiguration];
 
	NSError *error;
 
	// Input device
 
	AVCaptureDevice *captureDevice = [self frontOrDefaultCamera];
	AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];
 
	if ( [captureSession canAddInput:deviceInput] )
	{
		[captureSession addInput:deviceInput];
	}
 
	if ( [captureSession canSetSessionPreset:AVCaptureSessionPresetHigh] )
	{
		captureSession.sessionPreset = AVCaptureSessionPresetHigh;
	}
 
	// Video data output
 
	AVCaptureVideoDataOutput *videoDataOutput = [self createVideoDataOutput];
 
	if ( [captureSession canAddOutput:videoDataOutput] )
	{
		[captureSession addOutput:videoDataOutput];
 
		AVCaptureConnection *connection = videoDataOutput.connections[ 0 ];
 
		connection.videoOrientation = AVCaptureVideoOrientationPortrait;
	}
 
	// Metadata output
 
	AVCaptureMetadataOutput *metadataOutput = [self createMetadataOutput];
 
	if ( [captureSession canAddOutput:metadataOutput] )
	{
		[captureSession addOutput:metadataOutput];
 
		metadataOutput.metadataObjectTypes = [self metadataOutput:metadataOutput allowedObjectTypes:self.faceMetadataObjectTypes];
	}
 
	// Done
 
	[captureSession commitConfiguration];
 
	dispatch_async( _serialQueue,
				   ^{
					   [captureSession startRunning];
				   });
 
	_captureSession = captureSession;

All we’re doing here is creating an AVCaptureSesstion, adding an input device, adding an AVCaptureVideoDataOutput (so we can work with the frame buffer) and an AVCaptureMetadataOutput (to tell us about faces in the frame).

A few helper methods called during the setup:

- (AVCaptureMetadataOutput *)createMetadataOutput
{
	AVCaptureMetadataOutput *metadataOutput = [AVCaptureMetadataOutput new];
 
	[metadataOutput setMetadataObjectsDelegate:self queue:_serialQueue];
 
	return metadataOutput;
}
 
- (NSArray *)metadataOutput:(AVCaptureMetadataOutput *)metadataOutput
		 allowedObjectTypes:(NSArray *)objectTypes
{
	NSSet *available = [NSSet setWithArray:metadataOutput.availableMetadataObjectTypes];
 
	[available intersectsSet:[NSSet setWithArray:objectTypes]];
 
	return [available allObjects];
}
 
- (NSArray *)faceMetadataObjectTypes
{
	return @
	[
	 AVMetadataObjectTypeFace
	 ];
}
 
- (AVCaptureVideoDataOutput *)createVideoDataOutput
{
	AVCaptureVideoDataOutput *videoDataOutput = [AVCaptureVideoDataOutput new];
 
	[videoDataOutput setSampleBufferDelegate:self queue:_serialQueue];
 
	return videoDataOutput;
}

- (AVCaptureDevice *)frontOrDefaultCamera
{
	NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
 
	for ( AVCaptureDevice *device in devices )
	{
		if ( device.position == AVCaptureDevicePositionFront )
		{
			return device;
		}
	}
 
	return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
}

We’ve told AVCaptureMetadataOutput and AVCaptureVideoDataOutput to call us back (when we set “self” as the delegate) and to do it on a global queue called _serialQueue.  This is just to avoid any concurrency issues if the delegates are called from separate threads.  Here’s how we handle new metadata:

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
	_facesMetadata = metadataObjects;
}

Once we get a video frame, we’ll make a CIImage, mask the face with a CIFilter, then render the frame to an OpenGL ES 2.0 context:

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
	CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer( sampleBuffer );
 
	if ( pixelBuffer )
	{
		CFDictionaryRef attachments = CMCopyDictionaryOfAttachments( kCFAllocatorDefault, sampleBuffer, kCMAttachmentMode_ShouldPropagate );
		CIImage *ciImage = [[CIImage alloc] initWithCVPixelBuffer:pixelBuffer options:(__bridge NSDictionary *)attachments];
 
		if ( attachments ) CFRelease( attachments );
 
		CGRect extent = ciImage.extent;
 
		_filter.inputImage = ciImage;
		_filter.inputFacesMetadata = _facesMetadata;
 
		CIImage *output = _filter.outputImage;
 
		_filter.inputImage = nil;
		_filter.inputFacesMetadata = nil;
 
		dispatch_async( dispatch_get_main_queue(),
					   ^{
						   UIView *view = self.view;
						   CGRect bounds = view.bounds;
						   CGFloat scale = view.contentScaleFactor;
 
						   CGFloat extentFitWidth = extent.size.height / ( bounds.size.height / bounds.size.width );
						   CGRect extentFit = CGRectMake( ( extent.size.width - extentFitWidth ) / 2, 0, extentFitWidth, extent.size.height );
 
						   CGRect scaledBounds = CGRectMake( bounds.origin.x * scale, bounds.origin.y * scale, bounds.size.width * scale, bounds.size.height * scale );
 
						   [_ciContext drawImage:output inRect:scaledBounds fromRect:extentFit];
 
						   [_eaglContext presentRenderbuffer:GL_RENDERBUFFER];
						   [(GLKView *)self.view display];
					   });
	}
}

The _filter is an instance of CJCAnonymousFacesFilter. It’s a simple CIFilter that creates a mask from the faces metadata and a pixelated version of the image, then blends the result into the original image:

	// Create a pixellated version of the image
	[self.anonymize setValue:inputImage forKey:kCIInputImageKey];
 
	CIImage *maskImage = self.maskImage;
	CIImage *outputImage = nil;
 
	if ( maskImage )
	{
		// Blend the pixellated image, mask and original image
		[self.blend setValue:_anonymize.outputImage forKey:kCIInputImageKey];
		[_blend setValue:inputImage forKey:kCIInputBackgroundImageKey];
		[_blend setValue:self.maskImage forKey:kCIInputMaskImageKey];
 
		outputImage = _blend.outputImage;
 
		[_blend setValue:nil forKey:kCIInputImageKey];
		[_blend setValue:nil forKey:kCIInputBackgroundImageKey];
		[_blend setValue:nil forKey:kCIInputMaskImageKey];
	}
	else
	{
		outputImage = _anonymize.outputImage;
	}
 
	[_anonymize setValue:nil forKey:kCIInputImageKey];

You can find all the relevant code as gists on github:

- CJCViewController.h

- CJCViewController.m

- CJCAnonymousFacesFilter.h

- CJCAnonymousFacesFilter.m

Here’s a simple view engine for ASP.NET MVC that lets you use plain HTML for your views, even if it’s badly formed!  It supports a very simple attribute syntax for embedding other partial views in the page; those views can use whichever view engine you’d like (WebForms, Razor, NHaml etc).

Let’s start with the composite view engine; its job is to find a container view based on the Master page name, but also find a primary partial view based on the current MVC controller and action:

public interface ICompositeView
{
    IView PrimaryView { set; }
}

public abstract class CompositeViewEngine : VirtualPathProviderViewEngine
{
    private ViewEngineCollection otherViewEngines;

    public CompositeViewEngine()
    {
    }

    public override ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName, bool useCache )
    {
        if ( !controllerContext.IsChildAction )
        {
            var result = base.FindView( controllerContext, GetMasterName( controllerContext.RouteData.Values, masterName ), null, useCache );
            var compositeView = result.View as ICompositeView;

            if ( compositeView != null )
            {
                compositeView.PrimaryView = OtherViewEngines.FindPartialView( controllerContext, viewName ).View;

                return result;
            }
        }
        else
        {
            return OtherViewEngines.FindView( controllerContext, viewName, null );
        }

        return new ViewEngineResult( Enumerable.Empty<string>() );
    }

    public override ViewEngineResult FindPartialView( ControllerContext controllerContext, string partialViewName, bool useCache )
    {
        return new ViewEngineResult( Enumerable.Empty<string>() );
    }

    private ViewEngineCollection OtherViewEngines
    {
        get
        {
            lock ( this )
            {
                return ( otherViewEngines != null )
                    ? otherViewEngines
                    : otherViewEngines = new ViewEngineCollection( ViewEngines.Engines.Where( e => !( e is CompositeViewEngine ) ).ToList() );
            }
        }
    }

    protected virtual string GetMasterName( RouteValueDictionary routeValues, string defaultName )
    {
        return defaultName;
    }
}

After finding its own container view, it gives all other view engines the opportunity to find the contained partial.  Here’s the HTML view engine that derives from it:

public class HtmlViewEngine : CompositeViewEngine
{
    public IHtmlViewHelper Helper { get; set; }

    public HtmlViewEngine()
    {
        this.AreaViewLocationFormats = new string[]
        {
            "~/Areas/{2}/Views/{1}/{0}.html",
            "~/Areas/{2}/Views/{1}/{0}.htm",
            "~/Areas/{2}/Views/Shared/{0}.html",
            "~/Areas/{2}/Views/Shared/{0}.htm"
        };

        this.ViewLocationFormats = new string[]
        {
            "~/Views/{1}/{0}.html",
            "~/Views/{1}/{0}.htm",
            "~/Views/Shared/{0}.html",
            "~/Views/Shared/{0}.htm"
        };

        this.FileExtensions = new string[]
        {
            "html",
            "htm"
        };
    }

    protected override string GetMasterName( RouteValueDictionary routeValues, string defaultName )
    {
        return routeValues.ContainsKey( "path" )
            ? ( (string)routeValues[ "path" ] ).Split( '.' ).First()
            : !string.IsNullOrEmpty( defaultName ) ? defaultName : "Index";
    }

    protected override IView CreateView( ControllerContext controllerContext, string viewPath, string masterPath )
    {
        return new HtmlView( viewPath, Helper );
    }

    protected override IView CreatePartialView( ControllerContext controllerContext, string partialPath )
    {
        return new HtmlView( partialPath, Helper );
    }
}

Once the container view is found, it creates an HtmlView that knows how to render it.  Here’s how that looks (starting with a CompositeView):

public abstract class CompositeView : IView, ICompositeView
{
    protected string filename;

    public IView PrimaryView { get; set; }

    public CompositeView( string filename )
    {
        this.filename = filename;
    }

    public abstract void Render( ViewContext viewContext, TextWriter writer );
}
public interface IHtmlViewHelper
{
    void RenderContent( HtmlDocument document, ViewRenderer renderer );
}

public class HtmlView : CompositeView
{
    protected IHtmlViewHelper helper;
    protected HtmlDocument source;

    public HtmlView( string filename, IHtmlViewHelper helper )
        : base( filename )
    {
        this.helper = helper;
    }

    public override void Render( ViewContext viewContext, TextWriter writer )
    {
        var document = GetSource( viewContext );

        if ( helper != null )
        {
            var viewDataContainer = new ViewDataContainer( viewContext.ViewData.Model );
            var htmlHelper = new HtmlHelper( viewContext, viewDataContainer );

            helper.RenderContent( document, new ViewRenderer( viewContext, htmlHelper, PrimaryView ) );
        }

        document.Save( writer );
    }

    private HtmlDocument GetSource( ControllerContext controllerContext )
    {
        return source ?? ( source = GetSource( controllerContext.HttpContext, filename ) );
    }

    private HtmlDocument GetSource( HttpContextBase httpContext, string filename )
    {
        return httpContext.RequestCache().Cache( filename, () => LoadSource( httpContext, filename ) );
    }

    private HtmlDocument LoadSource( HttpContextBase httpContext, string filename )
    {
        var doc = new HtmlDocument();

        doc.Load( httpContext.Server.MapPath( filename ) );

        return doc;
    }
}

It uses HtmlAgilityPack to parse the HTML (with a little caching), injects new content into the DOM, then renders the result with some help from the ViewRenderer class (catches & pretty prints any rendering errors too):

public class ViewRenderer
{
    private ViewContext viewContext;
    private HtmlHelper htmlHelper;
    private IView primaryView;
    private ViewEngineCollection otherViewEngines;

    public ViewRenderer( ViewContext viewContext, HtmlHelper htmlHelper, IView primaryView )
    {
        this.viewContext = viewContext;
        this.htmlHelper = htmlHelper;
        this.primaryView = primaryView;
        this.otherViewEngines = new ViewEngineCollection( ViewEngines.Engines.Where( e => !( e is CompositeViewEngine ) ).ToList() );
    }

    public MvcHtmlString RenderContent( bool usePrimaryView, string actionName = null, string controllerName = null, string viewName = null )
    {
        var rendered = ( viewName != null )
            ? RenderView( viewName )
            : null;

        if ( rendered == null && usePrimaryView && ( controllerName == null || controllerName == (string)viewContext.RouteData.Values[ "controller" ] ) )
        {
            rendered = RenderView( primaryView );
        }

        if ( rendered == null ) rendered = RenderAction( actionName ?? "Index", controllerName );

        return rendered ?? MvcHtmlString.Empty;
    }

    public MvcHtmlString RenderView( string viewName )
    {
        return RenderView( FindView( viewName ) );
    }

    public MvcHtmlString RenderAction( string actionName, string controllerName = null )
    {
        MvcHtmlString result = null;

        try
        {
            result = htmlHelper.Action( actionName, controllerName );
        }
        catch ( HttpException ex )
        {
            result = MvcHtmlString.Create( ex.GetHtmlErrorMessage() ?? new HttpUnhandledException( ex.Message, ex.InnerException ).GetHtmlErrorMessage() );
        }
        catch ( Exception ex )
        {
            result = MvcHtmlString.Create( new HttpUnhandledException( ex.Message ).GetHtmlErrorMessage() );
        }

        return result;
    }

    private IView FindView( string viewName )
    {
        var result = otherViewEngines.FindPartialView( viewContext, viewName );

        return ( result.View != null ) ? result.View : null;
    }

    private MvcHtmlString RenderView( IView view )
    {
        if ( view == null ) return null;

        using ( var writer = new StringWriter() )
        {
            var renderViewContext = new ViewContext( viewContext, view, viewContext.ViewData, viewContext.TempData, writer );

            try
            {
                view.Render( renderViewContext, writer );
            }
            catch ( HttpException ex )
            {
                writer.Write( ex.GetHtmlErrorMessage() ?? new HttpUnhandledException( ex.Message, ex.InnerException ).GetHtmlErrorMessage() );
            }
            catch ( Exception ex )
            {
                writer.Write( new HttpUnhandledException( ex.Message ).GetHtmlErrorMessage() );
            }

            return MvcHtmlString.Create( writer.ToString() );
        }
    }
}

Finally we need to tell MVC about the view engine. Similar to the RouteConfig class you’ll see in a new MVC 4 project, here’s ViewEngineConfig:

public class ViewEngineConfig
{
    public static void RegisterEngines( ViewEngineCollection viewEngines )
    {
        viewEngines.Insert( 0, new HtmlViewEngine()
        {
            Helper = new HtmlViewHelper()
        } );
    }

    private class HtmlViewHelper : IHtmlViewHelper
    {
        public void RenderContent( HtmlDocument document, ViewRenderer renderer )
        {
            foreach ( var node in SelectNodes( document.DocumentNode, "//*[@html-primary or @html-controller or @html-action]" ) )
            {
                var isPrimary = node.GetAttributeValue( "html-primary", false );
                var controllerName = node.GetAttributeValue( "html-controller", null );
                var actionName = node.GetAttributeValue( "html-action", null );

                node.InnerHtml = renderer.RenderContent( isPrimary, actionName, controllerName ).ToHtmlString();
            }

            foreach ( var node in SelectNodes( document.DocumentNode, "//*[@html-partial]" ) )
            {
                node.InnerHtml = ( renderer.RenderView( node.Attributes[ "html-partial" ].Value ) ?? MvcHtmlString.Empty ).ToHtmlString();
            }
        }

        public string GetControllerName( HtmlDocument document )
        {
            var controllerNode = document.DocumentNode.SelectSingleNode( "//*[@html-controller]" );

            return ( controllerNode != null ) ? controllerNode.GetAttributeValue( "html-controller", null ) : null;
        }

        private static IEnumerable<HtmlNode> SelectNodes( HtmlNode node, string xpath )
        {
            return node.SelectNodes( xpath ) ?? Enumerable.Empty<HtmlNode>();
        }
    }
}

This is doing most of the content substitution.  It’s looking for a few pre-defined attributes in the HTML (html-primary, html-partial, html-controller and html-action) and replacing the content as needed.

Call RegisterEngines in Application_Start (in Global.asax.cs) and you’re done:

ViewEngineConfig.RegisterEngines( ViewEngines.Engines );

This works great when you’re just using regular MVC controller / action routes, but what if you want to handle direct requests for the HTML views? (for example if you’re hosting an entire static site within your MVC project… not as odd as it might sound).  We can do this by adding some Route definitions:

// Controller prefixed resources
routes.Add( "ControllerStaticResource", new Route( @"{controller}/{*path}", new StaticFileRouteHandler() )
{
    Constraints = new RouteValueDictionary( new { path = @".*\.(css|js|png|jpg|gif)" } ),
    Defaults = new RouteValueDictionary( new { rootFolder = "~/Views", folder = "Shared" } ),
} );

// Resources
routes.Add( "StaticResource", new Route( @"{*path}", new StaticFileRouteHandler() )
{
    Constraints = new RouteValueDictionary( new { path = @".*\.(css|js|png|jpg|gif)" } ),
    Defaults = new RouteValueDictionary( new { rootFolder = "~/Views", folder = "Shared" } )
} );

// Static HTML path with controller and action prefix
routes.Add( "ControllerActionStaticHtml", new PlaceholderRoute( @"{controller}/{action}/{*path}", handler )
{
    Constraints = new RouteValueDictionary( new { path = @".*\.(html|htm)" } ),
    Excludes = new[] { "path" }
} );

// Static HTML path with controller prefix
routes.Add( "ControllerStaticHtml", new PlaceholderRoute( @"{controller}/{*path}", handler )
{
    Constraints = new RouteValueDictionary( new { path = @".*\.(html|htm)" } ),
    Defaults = new RouteValueDictionary( new { controller = "Home", action = "Index", path = UrlParameter.Optional } ),
    Excludes = new[] { "path" }
} );

// Static HTML path with controller prefix
routes.Add( "StaticHtml", new PlaceholderRoute( @"{*path}", handler )
{
    Constraints = new RouteValueDictionary( new { path = @".*\.(html|htm)" } ),
    Defaults = new RouteValueDictionary( new { controller = "Home", action = "Index", path = UrlParameter.Optional } ),
    Excludes = new[] { "path" }
} );

// Static HTML path with controller and action prefix
routes.Add( "ControllerActionStaticHtmlGenerate", new PlaceholderRoute( @"{controller}/{action}/{path}", handler )
{
    Defaults = new RouteValueDictionary( new { controller = "Home", action = UrlParameter.Optional, path = UrlParameter.Optional } ),
    Placeholders = new RouteValueDictionary( new { action = "Index" } ),
    Excludes = new[] { "path" }
} );

This could be more complex than you need, so don’t freak out just yet Smile  The first couple are intercepting css, js, png etc files, and pointing them to a new StaticFileRouteHandler class.  Next we’re looking for .html and .html files, but letting the regular MvcRouteHandler take care of those.

Browsers expect non-absolute resource paths to be relative to the current request URL.  In this example we have CSS files, images etc in subfolders of the Views/Shared folder, along with the composite HTML files.  However the URL the browser sees might be just an MVC path.  The StaticFileRouteHandler lets us intercept those resource requests and grab the files from the appropriate place.

This isn’t a requirement (resources could be in the typical ~/Content folder if you prefer) but it can be pretty convenient. By keeping the embedded site files together they can be modified with any HTML editor. If a third party is responsible for those, you can just drop in the entire site when they make changes.

Here’s StaticFileRouteHandler:

public class StaticFileRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler( RequestContext requestContext )
    {
        return new StaticFileHttpHandler( requestContext );
    }

    public class StaticFileHttpHandler : IHttpAsyncHandler, IHttpHandler //, IRequiresSessionState
    {
        private delegate void AsyncProcessorDelegate( HttpContext httpContext );

        protected RequestContext requestContext;
        private AsyncProcessorDelegate asyncDelegate;

        public StaticFileHttpHandler( RequestContext requestContext )
        {
            this.requestContext = requestContext;
        }

        public void ProcessRequest( HttpContext context )
        {
            var routeValues = requestContext.RouteData.Values;

            var controllerName = (string)routeValues[ "controller" ];
            var folderName = (string)routeValues[ "folder" ];
            var path = GetFilePath( context );

            var filePath = ( controllerName != null )
                ? FindFilePath( controllerName, path ) ?? FindFilePath( folderName, path )
                : FindFilePath( folderName, path );

            if ( filePath != null )
            {
                var response = context.Response;

                response.ContentType = GetContentType( filePath );
                response.AddFileDependency( filePath );
                response.Cache.SetETagFromFileDependencies();
                response.Cache.SetLastModifiedFromFileDependencies();
                response.Cache.SetCacheability( HttpCacheability.Public );

                context.Response.TransmitFile( filePath );
            }
            else
            {
                System.Diagnostics.Trace.WriteLine( string.Format( "ERROR: StaticRouteHandler couldn't find {0}", context.Request.Url ) );
                context.Response.StatusCode = 404;
            }
        }

        private string GetFilePath( HttpContext context )
        {
            var routeValues = requestContext.RouteData.Values;

            if ( context.Request.UrlReferrer == null ) return (string)routeValues[ "path" ];

            var urlBase = "http://" + context.Request.Url.GetComponents( UriComponents.Host | UriComponents.Path, UriFormat.Unescaped );
            var referrerBase = "http://" + context.Request.UrlReferrer.GetComponents( UriComponents.Host | UriComponents.Path, UriFormat.Unescaped );

            var url = new Uri( urlBase, UriKind.Absolute );
            var referrer = new Uri( referrerBase, UriKind.Absolute );

            return referrer.MakeRelativeUri( url ).OriginalString;
        }

        private string FindFilePath( string folderName, string path )
        {
            var httpContext = requestContext.HttpContext;
            var routeValues = requestContext.RouteData.Values;

            var filePath = string.Format( "{0}/{1}/{2}",
                routeValues[ "rootFolder" ],
                folderName,
                path );

            var absolutePath = httpContext.Server.MapPath( filePath );

            System.Diagnostics.Trace.WriteLine( string.Format( "Looking for file in {0}", absolutePath ) );

            return File.Exists( absolutePath ) ? absolutePath : null;
        }

        private string GetContentType( string filePath )
        {
            var extension = System.IO.Path.GetExtension( filePath );

            switch ( extension )
            {
                case ".htm":
                case ".html": return "text/html";
                case ".css": return "text/css";
                case ".js": return "application/javascript";
                case ".png": return "image/png";
                case ".jpg": return "image/jpeg";
                case ".gif": return "image/gif";
            }

            return "text/plain";
        }

        public IAsyncResult BeginProcessRequest( HttpContext context, AsyncCallback cb, object extraData )
        {
            asyncDelegate = ProcessRequest;

            return asyncDelegate.BeginInvoke( context, cb, extraData );
        }

        public void EndProcessRequest( IAsyncResult result )
        {
            asyncDelegate.EndInvoke( result );
        }

        public bool IsReusable
        {
            get { return true; }
        }
    }
}

Full source and sample project coming soon! Smile

Anyone in or around Rochester, MN on November 14th 2013 with even the slightest interest in Xamarin development, should attend this…

http://rochmndotnetug201311-es2.eventbrite.com/?rank=1&sid=5e940311459b11e3a5871231391ec9bf

Smile

Looks like still time to register for TCCC15! – http://tccc15.eventbrite.com

It’s sure to be another awesome day Smile

ASP.NET MVC’s inbuilt Route class can handle just about anything you want.  However if you need a bit more control it’s easy to derive your own.  Here’s a simple class called DirectionalRoute that adds a couple of features:

  • CanGetRouteData: Set if this route can be used to parse a URL
  • CanGetVirtualPath: Set if this route can be used to generate a URL
  • Placeholders: Dictionary of placeholder values
  • Excludes: Array of keys to exclude from generated URL

The Placeholders dictionary needs a little explanation.  Setting Default values on a regular route works well, but if your RouteData contains a value that matches a default (or if the default is set to UrlParameter.Optional), it could be excluded completely from generated URLs. In situations where you really need a value to be present, set a Placeholder.

Here’s the code:

public class DirectionalRoute : Route
{
    public bool CanGetRouteData { get; set; }
    public bool CanGetVirtualPath { get; set; }
    public RouteValueDictionary Placeholders { get; set; }
    public string[] Excludes { get; set; }

    public DirectionalRoute(string url, IRouteHandler routeHandler)
        : this( url, null, null, null, routeHandler )
    {
    }

    public DirectionalRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
        : this( url, defaults, null, null, routeHandler )
    {
    }

    public DirectionalRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
        : this( url, defaults, constraints, null, routeHandler )
    {
    }

    public DirectionalRoute( string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler )
        : base( url, defaults, constraints, dataTokens, routeHandler )
    {
        this.CanGetRouteData = true;
        this.CanGetVirtualPath = true;
    }

    public override RouteData GetRouteData( System.Web.HttpContextBase httpContext )
    {
        if ( !CanGetRouteData ) return null;

        var routeData = base.GetRouteData( httpContext );

        if ( routeData != null && Placeholders != null )
        {
            var missing = routeData.Values
                .Where( rv => ( rv.Value == null || rv.Value == UrlParameter.Optional ) && Placeholders.ContainsKey( rv.Key ) )
                .ToArray();

            foreach ( var m in missing ) routeData.Values[ m.Key ] = Placeholders[ m.Key ];
        }

        return routeData;
    }

    public override VirtualPathData GetVirtualPath( RequestContext requestContext, RouteValueDictionary values )
    {
        return CanGetVirtualPath
            ? base.GetVirtualPath( GetRequestContext( requestContext ), GetRouteValues( values ) )
            : null;
    }

    private RequestContext GetRequestContext( RequestContext requestContext )
    {
        if ( Excludes == null || Excludes.Length == 0 ) return requestContext;

        var newRouteData = new RouteData( requestContext.RouteData.Route, requestContext.RouteData.RouteHandler );

        foreach ( var v in requestContext.RouteData.Values.Where( v => !Excludes.Contains( v.Key ) ) ) newRouteData.Values[ v.Key ] = v.Value;
        foreach ( var v in requestContext.RouteData.DataTokens ) newRouteData.DataTokens[ v.Key ] = v.Value;

        return new RequestContext( requestContext.HttpContext, newRouteData );
    }

    private RouteValueDictionary GetRouteValues( RouteValueDictionary values )
    {
        if ( Excludes == null || Excludes.Length == 0 ) return values;

        return new RouteValueDictionary( values.Where( v => !Excludes.Contains( v.Key ) ).ToDictionary( v => v.Key, v => v.Value ) );
    }
}

TCCC14 (off-by-one error?) takes place on April 27th.  Go register!  Even if you just want the free breakfast (donuts, coffee… what more does a geek need?), campus atmosphere and chance to win prizes, do it! Smile  It doesn’t even matter if some of the presentations aren’t in your usual fields; if you can’t decide, go to a random or fun-looking one!

http://www.twincitiescodecamp.com/TCCC/Default.aspx

I’ve been to the last two, and they’re great!

Follow

Get every new post delivered to your Inbox.

Join 41 other followers