Wednesday 2 July 2014

A little PeanutButter for your MVC

ASP.NET MVC is one of the best things that I could possibly think of to have happened to the web from a Windows-centric point of view. At last, there's a way to sweep that abomination that is WebForms under the proverbial carpet.

Like anything, though, it does have its caveats. You have useful constructs like Script and Style Bundles -- but no easy way to test them from a CI environment. Also script inclusion becomes a bit more manual than it needs to be when viewed though the lens of AMDs like require.js. But you do get the advantage of bundling in that a single request can satisfy multiple code/style requirements. (Let me be clear here: AMDs are good. I like require.js. But it does take a little more effort to set up and get working correctly and you don't (without even more configuration) get the hit-reduction that bundles provide. Both methods have their advantages. Select your tools for your tasks as they fit best for you, on the day.)

PeanutButter.MVC was built out of a need to make those processes slightly more testable and elegant.

First of all, there are two facade classes:
  • ScriptBundleFacade
  • StyleBundleFacade
They wrap ScriptBundle and StyleBundle accordingly and implement interfaces of the expected names (IScriptBundle and IStyleBundle). You would use them like you'd use ScriptBundle and StyleBundle instances from the MVC framework. However, since they implement an interface, you can also create substitutes for them so that you can test-constrain your bundle registration process. This is important because I found that it was not uncommon to add a new javascript or css file to the solution, build-and-run, and be surprised that my changes weren't in play -- until I realised that I hadn't bundled them.

For example, I have the following method on my BundleConfig:

public static void RegisterBundles(BundleCollection bundles, 
        Func<string, IScriptBundle> withScriptBundleCreator = null,
        Func<string, IStyleBundle> withStyleBundleCreator = null)
{
    withScriptBundleCreator = withScriptBundleCreator ?? ((bundleName) => new ScriptBundleFacade(bundleName));
    withStyleBundleCreator = withStyleBundleCreator ?? ((bundleName) => new StyleBundleFacade(bundleName));

    AddJQueryBundlesTo(bundles, withScriptBundleCreator);
    AddStyleBundlesTo(bundles, withStyleBundleCreator);
    AddHandlebarsBundleTo(bundles, withScriptBundleCreator);

    AddApplicationScriptBundlesTo(bundles, withScriptBundleCreator);
}

We can see that the function would ordinarily be invoked without lambda factories to produce Script- and StyleBundleFacades, so it produces its own, very straight-forward ones. However, the tests that constrain this method can inject lambda factories so that the bundling methods can be tested to ensure that they include the required bundles from the relevant sources. Indeed, the tests are quite straight-forward:
    [TestFixture]
    public class TestBundleConfig
    {
        private Func<string, IScriptBundle< CreateSubstituteScriptBundleCreator(List>IScriptBundle< withTrackingList)
        {
            return (name) =>
                {
                    var scriptBundle = Substitute.For<IScriptBundle>();
                    scriptBundle.Name.Returns(name);
                    var includedPaths = new List<string>();
                    var includedDirs = new List<IncludeDirectory>();
                    scriptBundle.IncludedPaths.ReturnsForAnyArgs(args =>
                        {
                            return includedPaths.ToArray();
                        });
                    scriptBundle.IncludedDirectories.ReturnsForAnyArgs(args =>
                        {
                            return includedDirs.ToArray();
                        });
                    scriptBundle.Include(Arg.Any<string>()).ReturnsForAnyArgs(args =>
                        {
                            includedPaths.AddRange(args[0] as string[]);
                            return new Bundle("~/");
                        });
                    scriptBundle.IncludeDirectory(Arg.Any<string>(), Arg.Any<string>())
                        .ReturnsForAnyArgs(args =>
                        {
                            includedDirs.Add(new IncludeDirectory(args[0] as string, args[1] as string));
                            return new Bundle("~/");
                        });
                    scriptBundle.IncludeDirectory(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>())
                        .ReturnsForAnyArgs(args =>
                        {
                            includedDirs.Add(new IncludeDirectory(args[0] as string, args[1] as string, (bool)args[2]));
                            return new Bundle("~/");
                        });
                    withTrackingList.Add(scriptBundle);
                    return scriptBundle;
                };
        }

        private Func<string, IStyleBundle> CreateSubstituteStyleBundleCreator(List<IStyleBundle> withTrackingList)
        {
            return (name) =>
            {
                var styleBundle = Substitute.For<IStyleBundle>();
                var includedPaths = new List<string>();
                styleBundle.IncludedPaths.ReturnsForAnyArgs(args =>
                    {
                        return includedPaths.ToArray();
                    });
                styleBundle.Include(Arg.Any<string>()).ReturnsForAnyArgs(args =>
                    {
                        var paths = args[0] as string[];
                        includedPaths.AddRange(paths);
                        return new Bundle("~/");
                    });
                withTrackingList.Add(styleBundle);
                return styleBundle;
            };
        }

        [Test]
        public void RegisterBundles_GivenBundleCollection_RegistersSharedScripts()
        {
            //---------------Set up test pack-------------------
            var collection = new BundleCollection();
            var scriptBundles = new List<IScriptBundle>();
            var styleBundles = new List<IStyleBundle>();
            //---------------Assert Precondition----------------

            //---------------Execute Test ----------------------
            BundleConfig.RegisterBundles(collection, 
                CreateSubstituteScriptBundleCreator(scriptBundles), 
                CreateSubstituteStyleBundleCreator(styleBundles));

            //---------------Test Result -----------------------
            Assert.AreNotEqual(0, scriptBundles.Count);
            Assert.AreNotEqual(0, styleBundles.Count);

            Assert.IsTrue(scriptBundles.Any(sb => sb.Name == "~/bundles/js/shared" &&
                                                    sb.IncludedDirectories.Any(d => d.Path == "~/Scripts/js/shared" && 
                                                                                    d.SearchPattern == "*.js" && 
                                                                                    d.SearchSubdirectories == true)));
        }
    }

All good and well. We can ensure that our MVC application is creating all of the required bundles. It would also be super-neat if we could streamline the inclusion process. Of course, we can.

PeanutButter.MVC also includes a utility called AutoInclude. If we decide to set up our bundles under /bundles/js/{controller} (for scripts for any action on the controller) and /bundles/js/{action}, then a lot of inclusion work can be done for us in our base _Layout view with a single line (assuming you've included the relevant @using clause at the top):


@AutoInclude.AutoIncludeScriptsFor(ViewContext)

AutoInclude uses the convention of scripts sitting under folders with names corresponding to the controller, with the casing of the scripts folders lowered to be more consistent with how script folders are named. This one line has, in conjunction with judicial bundling (and testing of that bundling!) allowed all views to just "magically" get their relevant scripts. In my project, I can create script bundles which include similarly-named folders and not have to worry about how my views get relevant logic scripts from there on out.

So, for example, I might perform registrations like the following (where scriptBundleCreator is a passed in Func<iscriptbundle>):


bundles.Add(scriptBundleCreator("~/bundles/js/policy")
    .IncludeDirectory("~/Scripts/js/policy", "*.js", false));
bundles.Add(scriptBundleCreator("~/bundles/js/policy/accept")
    .IncludeDirectory("~/Scripts/js/policy/accept", "*.js", false));
bundles.Add(scriptBundleCreator("~/bundles/js/policy/edit")
    .IncludeDirectory("~/Scripts/js/policy/edit", "*.js", false));

Now, from the Policy controller, I have two actions, Accept and Edit. Both have their relevant views, of course, and the AutoInclude is done automatically for them by virtue of the fact that they use the default _Layout.cshtml. Under my Scripts folder in my project, I have a file structure layout like:

policy
policy/common.js
policy/accept
policy/accept/accept.js
policy/accept/lead-autocompletion.js
policy/accept/proposalEmailer.js
policy/edit
policy/edit/clientDetailsDisplayUpdater.js
policy/edit/edit.js
policy/livePolicyUpdater.js

And the result is that the Policy/Accept view gets common.js, accept.js, lead-autocompletion.js and proposalEmailer.js. The Policy/Edit view gets common.js, clientDetailsDisplayUpdater.js, edit.js and livePolicyUpdater.js.

So now I'm free to create small, easily-testable javascript files (which I'll test with Jasmine and whatever works best for my purposes (eg karma or the Resharper unit test runner -- which works, mostly, with Jasmine, but has a few rough edges)). And when I want them in a page, I just drop them in the appropriate folder to get them on the next compile/debug run. And because of bundling, the end-user doesn't have to get many little hits for javascript files, instead, just getting two per view.

Apart from the testability of it and the simplicity of adding another piece of javascript functionality to the site, there's a huge bonus in grokkability. Let's face it: one of the reasosn why tests are good on your code is for when a new developer comes onto the project (or some unlucky person is tasked with maintaining some code they had nothing to do with). Tests provide feedback for when something breaks but also provide a communication mechanism for the new developer to figure out how discreet parts of the overall machine work. To the same end, understandable symbol and file naming and unsurprising project layout can really help with a new developer (or when you just have to get back on to the project for maintenance or extension and it's a couple of months down the line...)

Anyway, so there it is: PeanutButter.MVC. Free, small, doesn't depend on much, and hopefully useful. I'm certainly reaching for it the next time I'm in MVC land.

Tuesday 1 July 2014

INI files are dead... Long live INI files!

There was a time when INI files ruled the world of configuration. Since then, we've been told on numerous occasions by many people that we should rather be using XML. Or a SQLite database. Or something else, perhaps.

Now, don't get me wrong -- SQLite has its merits and XML is great if you want to store hierarchical data or if you need to configure your .NET application (which happens to already speak the lingo). But the reality is that INI serves quite well for a number of uses -- indeed, it can also be used to store hierarchical data, as you'd see if you checked out the innards of a .reg file. In particular, INI files are dead-easy to parse, both by machine and man -- and the latter is an advantage if you have nothing to hide and no need for quick read/write (where you might, for example, use SQLite). It's also a simple file-store so platform and library requirements are minimal. It's probably the easiest way to store structured configuration data and I still use it for projects unless I absolutely have to use something else.

A relatively small, simple part of the PeanutButter suite is the INI reader/writer/storage class PeanutButter.INI.INIFile. Usage is quite simple:


var ini = new INIFile("C:\\path\\to\\your\\iniFile.ini");
var someConfiguredValue = ini["colors"]["FavouriteColor"];
ini["Geometry"]["Left"] = "123";
ini.Persist();

In thesnippet above, we instantiate an INIFile class with a path to a file to use as the default persistence store. This file doesn't have to exist right now (and if it doesn't, it will be created with the Persist() call).

INIFile presents the data present in the source as a Dictionary<string, Dictionary<string, string>>, with indexing on the INIFile instance itself, making the syntax quite easy to use. Sections are created as and when you need them. Section and key names (such as "Geometry" and "Left" above) are case-insensitive to make access easier (and more compliant with the behavior of the older win32 calls for INI handling).

The parser tolerates empty lines and comments as well as empty keys (which are returned as an empty string).

Of course, you don't have to have a backing store to start with (or at all), and you can always override the output path with a parameter to Persist(). In addition, you can re-use the same INIFile, loading in a file from another path with the Load() method or loading with a pure string with the Parse() method.

Once again, the class has been developed on an as-required basis. It does much of what I want it to do (though I'd like it to persist comments on re-writing; that may come later). I hope that it can be of use to someone else too. I've lost count of how many times I've implemented an INI reader/writer. Hopefully, this is one of the last...

Update: I can't seem to comment on this article, so if you want to find out more (eg how to use INIFile), please check out the tests at GitHub which should illustrate usage. If there's not enough info there to use INIFile, please open an issue at the PeanutButter issue tracker so the conversation can be tracked for anyone else looking to use INIFile.

Thanks for reading!

What's new in PeanutButter?

Retrieving the post... Please hold. If the post doesn't load properly, you can check it out here: https://github.com/fluffynuts/blog/...