AngularJs HTML5 routing and IE9
We seem to be pushing the limits of AngularJS, and we've only started using it. The framework is very promising but definitely very new and a little rough in some areas.
We'd like to avoid hash urls in our solutions. The backend can respond to our routes just fine, and our backend framework is able to serve up the correct content (not necessarily the same content) for any area of the application. Using HTML5 mode and the history.pushState api, we can get clean URLs and no 404 errors on refresh...awesome.
Enter, IE9 (top light blue line). Our current browser standards are IE9+, Desktop/Android Chrome, FF, and IOS Safari. All browsers support history.pushState except IE9. The browser has had a recent significant drop in usage, but we can't ignore it quite yet.
At first, we thought we could set $locationProvider.html5Mode(true) and it would either a) break in IE9 calling a function that didn't exist (but we could define as window.location.assign(url)), or b) do a browser-based redirect based on window.location.assign. Well, RTFM: what it does is actually relatively painful. It falls back to hashbang syntax. This is similar to history.js, and IMHO, is broken in the same way as that library. Now you have URLs you can't share between browsers without crazy (and slow) workarounds.
I tried to work around the problem by providing a history.pushState implementation. This fooled Angular into thinking it was compliant, but caused some redirect loops and ultimately didn't work out. Next, I tried first intercepting routeChangeStart and calling event.preventDefault to no avail, then locationChangeStart also without success (I'm informed this does work, but for some reason I didn't see it, at least not in IE9).
Finally, it hit me...since we're not programmatically changing locations (just using anchor tags), we could simply determine if pushState is available and perform the following functions if it is not:
- Avoid defining any routes with the route provider
- Take our <script id='whateverroutewearecurrentlyon' type='text/ng-template'/> that is automatically generated on the backend for the current URL and manually add it to the ng-view element.
- Remove the ng-view attribute (for completeness) and add an ng-controller attribute pointing to the controller for the current route.
- Disable any pre-loading for pages other than the one on which we reside.
- Any URL used by history.pushState actually generates appropriate content from the server.
- The server at any URL provides the page layout and the application code. Providing the current page content in the script tag is nice (that's what we do) but it's not strictly required (#2 above could be done via Ajax).
- The application code is light enough that you (and your users) can put up with full page loads in non-compliant browsers until users upgrade. I wouldn't want to try this technique on gmail. ;-)
Enabling Quartz jobs in ASP.NET applications that will run despite restart
With IIS 7.5, you can now auto-start applications and have them continuously run. However, implementing System.Web.Hosting.IProcessHostPreloadClient means having a Process method with some significant restrictions. In a Spring.Net environment, the IOC Container's context is not yet started, and it will fail in rather spectacular ways if you try to crank it up with a hack. Even if you manage to do this, Quartz does not start up, so your jobs still will fail to run.
I spent some time on this problem and discovered that I do have access to System.Web.HttpRuntime.AppDomainAppVirtualPath, which allows us to automatically fire an initial request at the app automatically, if we're careful. This will crank up Application_Start and the rest of the Spring (and Quartz) machinery, allowing us to keep an app running 100% of the time. This initial request, however, will be thrown away, even if fired asynchronously, if initiated during the Preload() method. I ended up using a thread to get around this problem, allowing the requests to fire 500ms after the Preload method runs.
In certain cases, we can't just use http://localhost/yourvirtualpathhere. You might have multiple sites listening to different host headers, or you might be running SSL and don't want to hit the application using localhost. To cover those cases, I devised a web.config appSettings scheme where additional URLs can be applied to Preload (space delimited). You can also specify an IP address or machine name, so the web.config can stay static but different URLs can be applied as you move through environments. Here is the code:
public class Preloader : System.Web.Hosting.IProcessHostPreloadClient
{
public void Preload(string[] parameters)
{
var uris = System.Configuration.ConfigurationManager.AppSettings["AdditionalStartupUris"];
StartupApplication(AllUris(uris));
}
public void StartupApplication(IEnumerable<Uri> uris)
{
new System.Threading.Thread(o =>
{
System.Threading.Thread.Sleep(500);
foreach (var uri in (IEnumerable<Uri>)o) {
var client = new System.Net.WebClient();
client.DownloadStringAsync(uris.First());
}
}).Start(uris);
}
public IEnumerable<Uri> AllUris(string userConfiguration)
{
if (userConfiguration == null)
return GuessedUris();
return AllUris(userConfiguration.Split(' ')).Union(GuessedUris());
}
private IEnumerable<Uri> GuessedUris()
{
string path = System.Web.HttpRuntime.AppDomainAppVirtualPath;
if (path != null)
yield return new Uri("http://localhost" + path);
}
private IEnumerable<uri> AllUris(params string[] configurationParts)
{
return configurationParts
.Select(p => ParseConfiguration(p))
.Where(p => p.Item1)
.Select(p => ToUri(p.Item2))
.Where(u => u != null);
}
private Uri ToUri(string value)
{
try {
return new Uri(value);
}
catch (UriFormatException) {
return null;
}
}
private Tuple<bool string> ParseConfiguration(string part)
{
return new Tuple<bool, string>(IsRelevant(part), ParsePart(part));
}
private string ParsePart(string part)
{
// We expect IPv4 or MachineName followed by |
var portions = part.Split('|');
return portions.Last();
}
private bool IsRelevant(string part)
{
var portions = part.Split('|');
return
portions.Count() == 1 ||
portions[0] == System.Environment.MachineName ||
HostIpAddresses().Any(a => a == portions[0]);
}
private IEnumerable<string> HostIpAddresses()
{
var adaptors = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces();
return adaptors
.Where(a => a.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up)
.SelectMany(a => a.GetIPProperties().UnicastAddresses)
.Where(a => a.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
.Select(a => a.Address.ToString());
}
}
Installing Cyanogenmod 7.2 on Verizon Droid 2 Running 2.3.4
They said it couldn't be done. If you install 2.3.4 OTA update from Verizon, you were stuck with a version that was un-rootable. And rooting is the first step in getting a custom Android build on a phone. Specifically:
There is currently (as of May 2012) no way to root a Droid 2 with this system version.As a word of warning, the procedures for getting Cyanodenmod are as scary as the link above, and with 2.3.4, you're really in the deep end. Here's a summary of what I did. I'm not providing step-by-step directions for two reasons; first, if you can't get there from the summary, you're probably in over your head and you have a good chance of bricking your phone. Secondly, I am providing links to my source materials with more details. I'm also taking you through the method I went, which generally used tools rather than adb commands/doing it by hand. I purchased two of the tools...I think the $8 I spent was worth it, but go read the wiki if you want to do it the hard way.
Rooting
Custom Recovery ROM and Bootloader
- Bootloader is run, presumably by the hardware
- Bootloader looks around, checks out everything, and loads the ROM image. Depending on the environment it can run the recovery ROM or the normal boot ROM (your phone).
Prepping and Hacking the Cyanogen install
- Cyanogen latest version: http://wiki.cyanogenmod.org/wiki/Devices_Overview#Motorola_Droid_2. Right now, this is 7.2.0, available at: http://get.cm/get/jenkins/2824/cm-7.2.0-droid2.zip
- Google apps (gets you the market): http://goo-inside.me/gapps/gapps-gb-20110828-signed.zip. That is the mirror link as the primary didn't work for me. The cyanogenmod page on this is at: http://wiki.cyanogenmod.org/wiki/Latest_Version#Google_Apps
Installing Cyanogen
- Back up your existing ROM
- Select and install the Cyanogen ROM
- Select and install the Google Apps ROM (I think this just unpacks apks and puts them in a special directory, but I'm not sure). I thought it was way weird I was installing two ROMs.
- Lastly, wipe all cache/data. I don't remember if I performed factory reset - I think I did not. Apparently the phone will get into a "boot loop" unless this step is performed, but if you forget for some reason, you can get out of that problem: http://forum.xda-developers.com/showthread.php?t=1832130
Other thoughts
Update
The constraint for route parameter 'httpMethod' on the route with URL '{controller}/{id}' must have a string value in order to use an HttpMethodConstraint.
Wow...what did that mean? Here I am, just using @Url.Action (and BeginForm and any other method that walks the route table backwards). My application is RESTful, so most routes have an HttpMethodConstraint so they only match if the method is correct.
I could not wrap my head around this message and ended up using JustDecompile to pull open System.Web.Routing and had a look at the HttpMethodConstraint Match method. You'll note that the routeDirection parameter tells the object whether to match based on an incoming request (the normal case) or for Url Generation (used for Url.Action, BeginForm and the like).
Pulling open the source, I found this:
switch (routeDirection1)
{
case RouteDirection.IncomingRequest:
{
ICollection<string> allowedMethods = this.AllowedMethods;
if (func == null)
{
func = (string method) => string.Equals(method, httpContext.Request.HttpMethod, StringComparison.OrdinalIgnoreCase);
}
return allowedMethods.Any<string>(func);
}
case RouteDirection.UrlGeneration:
{
if (values.TryGetValue(parameterName, out obj))
{
string str = obj as string;
if (str != null)
{
return this.AllowedMethods.Any<string>((string method) => string.Equals(method, str, StringComparison.OrdinalIgnoreCase));
}
else
{
object[] url = new object[2];
url[0] = parameterName;
url[1] = route.Url;
throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, RoutingResources.HttpMethodConstraint_ParameterValueMustBeString, url));
}
}
else
{
return true;
}
}
}
The RouteDirection.UrlGeneration case block represents what happens when the HttpMethodConstraint is asked to match for a generated Url. The InvalidOperationException represents the error message we see on the YSOD.
Even this took me a bit to work through, but the bottom line is that it's finding the parameter name from the route and looking for the value of that parameter name from the input provided to it. Given the following route:
routes.MapRoute("UpdateByPut",
"{controller}/{id}",
new { action = "Update" },
new { httpMethod = new HttpMethodConstraint("PUT") }
);
HttpMethodConstraint will find httpMethod (this is the value of parameterName in the code above), and look for a route value of httpMethod, expecting it to be a string. If it doesn't, it will throw the exception. So, using the route above with the following code:
@Url.Action("Show", "MyEntity", new { id = Model.Id })
Won't work. Instead, you need this:
@Url.Action("Show", "MyEntity", new { id = Model.Id, httpMethod = "GET" })
Also note that the property name httpMethod in the @Url.Action line above comes from the same property name I used in routes.MapRoute within Global.asax.cs. This is one of the few times that MVC cares about the property name used on a constraint.
Once you understand what the error message is trying to tell you it's all pretty clear. You just need to be a rocket scientist the first time through.
EduSpring Part 6: Crazy Error Messages and What to do about them
- The virtual path '/currentcontext.dummy' maps to another application, which is not allowed: This error message usually means you've deployed the application to a server, but forgotten to make the virtual directory an application in IIS. Spring performs a server.Transfer() call to "~/currentcontext.dummy" to get all the dependency wiring done correctly before another server.Transfer() brings the request back to the right place.
- no application context for virtual path: This error message tells you that there's no spring configuration setup at all.
- Resource handler for the 'web' protocol is not defined: This message tells you that the httpModule is not active. Add the spring.net HttpModule into the appropriate section of web.config (system.web for IIS6, system.webserver for IIS7).
- Could not load type from assembly: This may or may not have anything to do with Spring, so take a look a the stack trace. If you see Spring.Core (specifically Spring.Core.TypeResolution.TypeResolver.Resolve) in the trace, you can be fairly certain you misspelled a type name in the configuration.
- node cannot be resolved for the specified context: Spring tried to assign an object or value to a property, but the property doesn't exist on the object. It's likely you misspelled the property name, or maybe you refactored, removed the property, but forgot to update the spring configuration.
- Is your configuration binding the right type? For example, if your property is a string, and you're assigning a stringbuilder to it, Spring will just ignore the assignment.
- You're missing the object definition in the spring configuration.
- You're missing the property definition in the spring configuration.
- Did you commit the cardinal sin of DI? Do not use new MyType()!
Labels: EduSpring | 0 Comments
EduSpring Part 5: Spring.Net in an ASP.NET environment (including MVC)
This post is part of a series on Spring.NET. I recommend starting at the beginning if you haven't already. Also, I am walking through code in the accompanying GitHub project.
By now, you should have the basics of DI, IoC, and the benefits and drawbacks of the approach. Now, I'll introduce you to the architecture of Spring.NET in an ASP.NET environment. I'm sure a lot of other IoC frameworks operate in a similar manner. If not, you can add code to make them work that way. ;-)
If you look at the IocWithoutSpring project, you'll see this Main function:
static void Main(string[] args)
{
//These two lines are handled by the Spring.NET HttpModule
var container = new IoCContainer();
container.Initialize();
// These two lines are also handled by the HttpModule by a special
// syntax in the spring configuration
var service = new DoSomeWork();
service.Worker = container.GetObject("myObject");
// This is what the ASP.NET Framework would do
Console.WriteLine("The output is: " + service.DoTheWork());
}
Most of the time, creation of the IoC container itself is a single dependency that's particularly hard to get rid of without writing some reflection-style glue code. Ideally, we want our objects to be completely ignorant of this container, though. In ASP.NET, the web.config provides us with the concept of an HttpModule, which will look at every request coming from into the web server and have an opportunity to do something with it. Taking advantage of this feature, the Spring.Net team wrote an ASP.NET HttpModule that will do just that, so the first two lines of main (instantiation and initialization of the container) are handled by the ASP.NET framework. Awesome!
Now our Spring.Net dictionary is populated, assuming we have a valid configuration. I'll address more about the pain points of Spring configuration later, but this very early creation of lots of objects is one of the main frustrations of people who want to use Spring.Net. The next question is, what about setting up dependencies in ASPX pages?
Technically speaking, the ASP.NET framework parses an ASPX file (or MVC view) and code-generates a class. On each request, it creates an instance of this class and allows it to process the request, before destroying the object. Very stateless, but this creation and destruction of classes rubs against IoC's Dictionary<string,object> heart.
If you read this contract from the other side (a.k.a. how would I solve this problem if I were writing Spring.Net), you can imagine yourself writing a PageHandlerFactory that can deliver the aspx page class with dependencies already injected. There are two problems you have to solve, however:
- What name do you use to look up the object?
- How do you deal with request-specific data?
Labels: EduSpring | 0 Comments
EduSpring Part 4: What is so terribly broken with Dependency Injection?
Move all the CS BS to the side. This stuff doesn't work in the real world. And it's because of things that could have (and probably should have) been fixed by uber-geeks 10+ years ago with fancy CompSci PhDs. And I'm talking about .Net specifically here, although I can throw the same stones at Java. Most (but not all) other languages have the same problems I'll get on my soapbox about here.
To do DI correctly, you have one of two options:
- Your class has dependencies on properties/internal fields that implement Interfaces
- Your class has dependencies on properties/internal fields that implement base classes
- Do you assume anything about the return value? It might return what you expect, or might return null. Or, it could throw an exception. Generally, experience with DI will teach you good defensive coding, but it does take work...
- Can the method handle null input values? If it errors, is it going to return null or throw an exception? If it throws, what kind of exception will it generate?
- Is the method you're calling going to muck with the object you pass in? If it does alter properties, is that a problem? What if you're not in control of the object, and the property this dependency decides it's OK to muck with throws an exception during the set operation?
Labels: EduSpring | 0 Comments
