You can create a precompiled assembly with view classes at a high level with the a batch descriptor and a SparkViewFactory, or at a low level with an array of view descriptors and a SparkViewEngine.
If you provide a high-level batch descriptor to the asp.net mvc or castle monorail SparkViewFactory it will be converted into an array of descriptors based on which view files exist. Those will then be passed to the SparkViewEngine.
public static void PrecompileViews(ControllerBuilder builder) { var controllerFactory = (SparkControllerFactory)builder.GetControllerFactory(); var viewFactory = new SparkViewFactory(controllerFactory.Settings); var batch = new SparkBatchDescriptor(); batch .For<HomeController>() .For<ProductsController>(); viewFactory.Precompile(batch); }
A static method like this would be called from Global Application_Start. The tricky part is getting your hands on the instance of the view factory.
//todo - about the same. just need sample codeIf you create an instance of the SparkViewEngine you can call BatchCompilation directly. It takes the name of the target assembly, which should have the .dll extension, and a list of SparkViewDescriptor which have been filled in. The SparkViewEngine will need to have the ViewFolder property set.
var engine = new SparkViewEngine(settings) { ViewFolder = new FileSystemViewFolder(viewsLocation) }; engine.BatchCompilation(targetPath, Global.AllKnownDescriptors());
With Asp.Net Mvc the view factory is fairly easy to create when you need it. The tricky part can be making sure the path to the views folder is correct. You could pass in settings or assume that the spark section in the app.config in your test project is correct.
var viewFactory = new SparkViewFactory(settings) { ViewSourceLoader = new FileSystemViewSourceLoader(@"..\..\..\NorthwindDemo\Views") }; var batch = new SparkBatchDescriptor(); batch.For<ProductsController>(); viewFactory.Precompile(batch);
The return value of Precompile and BatchCompilation is the resulting loaded Assembly. You could continue to make assertions about the number of types in the file.
This one is probably the coolest one. Especially if you're developing a web app to deploy to a medium trust shared environment. You can find some examples of this in the Samples\AspNetMvc\PrecompiledViews project and Samples\DirectUsage\MediumTrustHosting project.
First: Add the following to the web.config. This will cause an exception to be thrown if your web app ever tries to compile a view at runtime, which will help avoid a situation where you work in development (because the view will generate on the fly) but not in production.
<system.web> <trust level="Medium" /> ...
Second: Add a new Installer Class item to your web project. It will have a grey designer surface. From the toolbox drag a new instance of either the MvcContrib.SparkViewEngine.Install.PrecompileInstaller or the Castle.MonoRail.Views.Spark.Install.PrecompileInstaller onto this surface.
(If it's not in the toolbox you may need to right-click one of the categories, say "Choose Items...", and Browse... to locate the assembly that contains the tool you want.)
Third: Select the precompileInstaller1 you just added. If you like you can provide a target assembly file name - by default it will add .Views.dll to your web app assembly name. Then choose the events in the properties (yellow lightning) and double-click "DescribeBatch". Provide the implementation, for example:
private void precompileInstaller1_DescribeBatch(object sender, DescribeBatchEventArgs e) { e.Batch .For<HomeController>() .For<HomeController>().Layout("Ajax").Include("_Notification") .For<AccountController>(); }
You could also add a static to Global and have this event handler call that static if you don't want to bury information like this in an event on a designer of an installer component. You could also then re-use that function from a unit test that compiles everything also.
Fourth and finally: Right-click the web project, choose properties, and change the Build Events. Add the following Post-build event command line:
%systemroot%\Microsoft.NET\Framework\v2.0.50727\installutil "$(TargetPath)"
Done! Now every time you hit Build, or F5-Debug/Run, or run unit tests from visual studio you'll automatically generate the precompiled views assembly in the bin directory. Plus, as icing on the cake, if there are any errors that occur in the installer they'll show up in the build output.
------ Build started: Project: PrecompiledViews, Configuration: Debug Any CPU ------ PrecompiledViews -> E:\Projects\spark\src\Samples\AspNetMvc\PrecompiledViews\bin\PrecompiledViews.dll %systemroot%\Microsoft.NET\Framework\v2.0.50727\installutil "E:\Projects\spark\src\Samples\AspNetMvc\PrecompiledViews\bin\PrecompiledViews.dll" Microsoft (R) .NET Framework Installation utility Version 2.0.50727.1433 Copyright (c) Microsoft Corporation. All rights reserved. ...etc... An exception occurred during the Install phase. Spark.Compiler.CompilerException: Dynamic view compilation failed. generated.cs(20,22): error CS0103: The name 'hi' does not exist in the current context
When the app loads, usually in Global.Application_Start, you can load an existing precompiled view assembly instead of generating one. You would need to call SparkViewEngine's LoadCompilation(Assembly precompiled) method. It will enumerate all of the public types in the assembly to recreate and cache the appropriate SparkViewDescriptors. The array of descriptors which were loaded are returned.
public static void LoadPrecompiledViews(SparkViewFactory factory) { factory.Engine.LoadBatchCompilation(Assembly.Load("MyWebApp.Views.dll")); }
As always, the tricky part can be getting your hands on an instance of the SparkViewFactory.
There is no dynamic recompilation of classes that are loaded in this way and changes to the spark files will have no effect. However the files must be available on disk at runtime. Sorry about that - but the different frameworks still needs to test for the view files' existence to know what view descriptor it should instantiate.
The batch descriptor contains multiple entries and is built with a fluent interface. The target assembly name can be provided on the constructor, otherwise the assembly will be named after a guid and go to the temporary asp.net files directory.
Each call to .For<YourController>() starts a new rule entry. Within a rule you can call Layout, Include, and Exclude multiple times. Include and Exclude can have patterns that end with *. If the pattern ends with * it won't find things in "Shared". The other special case is the single character "*" which matches anything that doesn't start with an underscore.
batch .For<HomeController> .For<HomeController>.Layout("ajax").Include("_*") .For<ProductsController>.Include("Index").Include("List").Include("Detail") .For<AccountController>.Include("*").Exclude("TestMessage").Exclude("Blah");
If Include is not called for a rule the default is to include all views that don't start with a "_". If Layout is not called the default is to use the layout that would be used automatically appropriate for the framework in question. If you call .Layout multiple times it'll generate a class for each combination of layout and view. That's if you want to precompile several themes where the controller will specify the layout at runtime.
A call to layout can take an array of strings. This is not the same as calling Layout multiple times - rather it's to support MonoRail's ability to organize the layout into several layers.