Monday, December 12, 2011
Saturday, August 13, 2011
Password insanity
Tonight I had to fill out some official paperwork and went online to get it done. (Before I start griping – the online form was fine, I could fill it out with minimal problems and got a nice PDF with all the entered info at the end.)
But to get to the form – oh boy.
I’d been to this site before, so I knew I had an account – I guessed my password – err. Ok, time to hit the forgot password link.
Oh – ok, “the password expires every 60 days”, so that’s why. I enter the answer to my “secret” question (the answer to which is a matter of public record, and would probably take a hacker 5 minutes to figure out) and am allowed to attempt to enter my new password. Err. “Your password can not contain more than three consecutive letters from your old password”.
Alright odd, but, attempt 2. Err. “Your password must be at least 8 characters”.
Ok, fine – should have guessed that. Attempt 3. Err. “Your password must contain a special character AND two entries from the three groups: number, upper case and lowercase.”
Uhm – ok?. Attempt 4. Err. “Your password must begin and end with a letter.”
WTF? Attempt 5: I enter an upper case letter, a set of adjacent keyboard symbols, and a lower case letter and lo and behold the password is accepted.
Don’t ask me what the password was – even if I WOULD tell you, I couldn’t – I have already forgotten. But that’s fine, next time I’ll just repeat the same exercise and get in by answering my “secret” question.
XKCD says it oh so well:
Labels: errors, me-me-me, programming, rant, security, silliness, usability
Saturday, April 09, 2011
Someone's hacking my Blogger account
Sunday, June 13, 2010
IE 9 HTML5 Testing: “Works on My Machine”
One of my esteemed colleagues on an internal forum posted about how great IE 9’s HTML5 support was, based on the result of Microsoft’s test pages. MS’s tests are sadly self-selective however: meaning they only seem to test for elements that IE9 supports: http://samples.msdn.microsoft.com/ietestcenter/
W3C Web Standards | Number of Submitted Tests | Internet Explorer 9 Platform Preview | Mozilla Firefox 3.6.3 | Opera 10.52 | Apple Safari 4.05 | Google Chrome 4.1 |
---|---|---|---|---|---|---|
HTML5 | 40 | 78% | 63% | 48% | 43% | 43% |
...” |
Compare that to my own results running html5test.com on the 6(!) browsers I have installed:
Html5 is the first time in a decade that the browser vendors have had a new major standard to fight over; I’m just grateful that this time around we’ll have an army of frameworks such as jQuery that can level the development playing field for us.
Labels: advertising, apple, errors, experiment, firefox, google, html5, microsoft, rant, testing
Sunday, May 09, 2010
Oops - “spellcheck any multiligual texts…”
So I was responding to an internal developer forum request for recommendations for a WYSIWYG html editor with spell check. I was going to recommend Telerik’s Editor and related RadSpell component. Not so sure any more…
Cobbler. Children. All that.
Labels: advertising, asp.net, errors, failure, random, silliness
Wednesday, March 17, 2010
Monday, November 09, 2009
SharePoint Office Server 2010, on Windows 7, in Google Chrome
It took a few hours of downloading, a few hours of installing (with workarounds), but I now have SharePoint 2010 Office Server Beta (aka the MOSS upgrade) running on my Windows 7 laptop; and as can be seen, it renders beautifully in Google Chrome. <UPDATE1> installed and uninstalled the dang thing several times, with the same result: I keep getting WCF errors:
System.Configuration.ConfigurationErrorsException: Unrecognized attribute 'allowInsecureTransport'. Note that attribute names are case-sensitive. (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\WebClients\Profile\client.config line 56)
</UPDATE1>
<UPDATE2>
I completely uninstalled every bit of SharePoint 2010, Visual Studio 2010, .net 4, and every prerequisite of SP. Then reinstalled. This time I ran the configuration wizard (as admin) while connected to the domain controller (via vpn). MOST of SP2010 now seems to work.
I then tried adding the local SP admin account (hat I had created for my first install) to the Farm Administrators group. Same damn WCF error.
So I simply backed up then edited client.config to remove the offensive attribute both from line 57 and from a second instance on line 97. Then I rebooted and tried the Farm Admin Addition again.
Same error message (note that I am NOT running SPF, I am running the full Office Server version)…
… but different cause this time:
Local administrator privilege is required to update the Farm Administrators' group.
Could this be a limitation of running SharePoint2010 on Windows 7?
</UPDATE2>
The one Another challenge of the installation was that the Configuration wizard would fail repeatedly on step 2 (of 10) when attempting to set up the configuration database. The error was “user can not be found”. I then noticed that my domain account did not have full admin rights on my SQL server instance, unless I started SQL Management Studio as Run as Admin.
The workaround I used was to create a local admin account, grant that account full db access, switch to the new admin account, run the configuration wizard, and then switch back to my regular user account. Crude, but effective.
<UPDATE2>
The better alternative is to connect to your corporate network to gain access to your domain controller. On my 3rd, 4th and 5th (!) time running the configuration wizard, this was the approach I took, and it seems to work well. (I understand this is also something that is necessary for the VS 2010 TFS install.)</UPDATE2>
I have no opinions on speed, etc – more of that to come, I’m sure.
<UPDATE3>
So I finally ended up scrapping Windows 7 AND 2008 R2. With our code, there just was no getting around compatibility issues. So for now I am running on good ol’ Windows Server 2008 x64.
Can’t wait for MS to release bootable, sysprepped developer vhds for this...
</UPDATE3>
Labels: errors, howto, microsoft, moss, sharepoint, SharePoint2010, windows7
Wednesday, November 04, 2009
This is Business Productivity?
Really, Microsoft? This is the appropriate stock-photo for business productivity?
Are they productive because they are wearing suits?
Are they watching other people work productively?
Is the business being productive without them?
Did they just finish being productive and are now basking in the glow of their success?
(from http://go.microsoft.com/?linkid=9690494)
Labels: advertising, errors, microsoft, moss, rant, sharepoint, silliness
Tuesday, October 27, 2009
Subtle Google Map/Search Usability Bug
When searching for a company in Google, if Google can determine a map result it’ll show it, along with a crowd-sourcing link to let you verify the accuracy of the business listing:
Only problem is that the confirmation message asks the OPPOSITE of the original question:
…
Sunday, September 27, 2009
Friday, August 28, 2009
Making T4MVC comply with CLS
FXCop rule CA1014 tells you to mark your assembly as CLSCompliant. If you adhere to this, your T4MVC (as of build 2.4.01 at least) will throw compiler warnings saying stuff like
Identifier | ‘xxxController._Actions’ | is not CLS-Compliant. |
If you have 10 controllers and 50 views this will result in 61 warnings…
The reason is that these are public members that start with an underscore, which is a CLS no-no:
http://stackoverflow.com/questions/1195030/why-is-this-name-not-cls-compliant
To solve this, edit the T4MVC.tt file to mark the code with a [CLSCompliant(false)] attribute. Once you start this, you’ll also find additional warnings from mebers that implement the now-explicitly-non-compliant members, but a few more [CLSCompliant(false)] attribute handles that. Full code in gist below.
<# | |
/* | |
T4MVC Version 2.4.01 + AIS Modifications | |
AIS Modifications are to fix StyleCop and CLSCompliant-related issues: | |
* Marking the file with an // <auto-generated /> comment | |
* Changing the region to include the term 'Generated Code' | |
* Marking all public members that start with _ as CLSCompliant(false) | |
* Marking all members that reference said members as CLSCompliant(false) | |
Full details at http://mo.notono.us/2009/08/making-t4mvc-comply-with-stylecop.html | |
and http://mo.notono.us/2009/08/making-t4mvc-comply-with-cls.html | |
Find latest version on http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=24471&ProjectName=aspnet | |
Written by David Ebbo, with much feedback from the MVC community (thanks all!) | |
david.ebbo@microsoft.com | |
http://twitter.com/davidebbo | |
http://blogs.msdn.com/davidebb | |
Related blog posts: | |
http://blogs.msdn.com/davidebb/archive/2009/07/28/t4mvc-2-4-updates-settings-file-sub-view-folders-actionname-support-and-more.aspx | |
http://blogs.msdn.com/davidebb/archive/2009/06/30/t4mvc-2-2-update-routing-forms-di-container-fixes.aspx | |
http://blogs.msdn.com/davidebb/archive/2009/06/26/the-mvc-t4-template-is-now-up-on-codeplex-and-it-does-change-your-code-a-bit.aspx | |
http://blogs.msdn.com/davidebb/archive/2009/06/17/a-new-and-improved-asp-net-mvc-t4-template.aspx | |
Feel free to use and modify to fit your needs. | |
This T4 template for ASP.NET MVC apps creates strongly typed helpers that eliminate the use | |
of literal strings when referring the controllers, actions and views. | |
To use it, simply copy it and T4MVC.settings.t4 to the root of your MVC application. | |
This will enable the following scenarios: | |
Refer to controller, action and view names as shown in these examples: | |
- MVC.Dinners.Name: "Dinners" (controller name). | |
- MVC.Dinners.Views.DinnerForm: "DinnerForm" (view name) | |
- MVC.Dinners.Actions.Delete: "Delete" (action name) | |
Strong type certain scenarios that refer to controller actions. e.g. | |
- Html.ActionLink("Delete Dinner", MVC.Dinners.Delete(Model.DinnerID)) | |
- Url.Action(MVC.Dinners.Delete(Model.DinnerID)) | |
- RedirectToAction(MVC.Dinners.Delete(dinner.DinnerID)) | |
- Route defaults e.g. | |
routes.MapRoute( | |
"UpcomingDinners", | |
"Dinners/Page/{page}", | |
MVC.Dinners.Index(null) | |
); | |
Refer to your static images and script files with strong typing, e.g. | |
Instead of <img src="/Content/nerd.jpg" ...>, you can write <img src="<%= Links.Content.nerd_jpg %>" ...> | |
Instead of <script src="/Scripts/Map.js" ...>, you can write <script src="<%= Links.Scripts.Map_js %>" ...> | |
Or if the file name is dynamic, you can write: Links.Content.Url("foo.jpg") | |
KNOWN ISSUES: | |
- Users running VisualSVN have reported some errors when T4MVC tries to change actions to virtual and controllers to partial. | |
The suggestion when that happens is to manually make those changes. This is just a one time thing you need to do. | |
- It will not locate controllers that live in a different project or assembly | |
HISTORY: | |
2.4.01 (07-29-2009): | |
- Put all the generated code in a T4MVC #region. This is useful to tell tools like ReSharper to ignore it. | |
- Fixed issue where controller methods returning generic types cause template to blow up | |
- Added a setting in T4MVC.settings.t4 to turn off the behavior that always keeps the template dirty | |
2.4.00 (07-28-2009): | |
- Added support for configurable settings in a separate T4MVC.settings.t4 file | |
- Added a parameter-less pseudo-action for every action that doesn't already have a parameter-less overload | |
- Added support for having T4MVC.tt in a sub folder instead of always at the root of the project | |
- Fixed issue when a base controller doesn't have a default ctor | |
- Added T4Extensions into System.Web.Mvc namespace to fix ambiguous resolution issue | |
- Misc cleanup | |
2.3.01 (07-10-2009): | |
- Fixed issue with [ActionName] attribute set to non literal string values (e.g. [ActionName(SomeConst + "Abc")]) | |
- Fixed duplication issue when partial controller classes have a base type which contains action methods | |
- Skip App_LocalResources when processing views | |
- Cleaned up rendering logic | |
2.3.00 (07-07-2009): | |
- Added support for sub view folders | |
- Added support for [ActionName] attribute | |
- Improved handling when the controller comes from a different project | |
- Don't try to process generic controller classes | |
2.2.03 (07-06-2009): | |
- Added support for action methods defined on controller base classes | |
- Improved error handling when not able to change actions to virtual and controllers to partial | |
2.2.02 (07-01-2009): | |
- Fixed break caused by incorrect support for derived ActionResult types in 2.2.01 | |
- Fixed issue with duplicate view tokens getting generated when you have both foo.aspx and foo.ascx | |
2.2.01 (07-01-2009): | |
- Added support for action methods that return a type derived from ActionResult (as opposed to exactly an ActionResult) | |
- Fixed issue when controller is using partial classes | |
- Fixed folder handling logic to deal with generated files | |
- Fixed issue with folder names that are C# keyword | |
- Throw NotSupportedException instead of NotImplementedException to avoid being viewed as a TODO | |
2.2.00 (06-30-2009): | |
- Added strongly typed support to MapRoute | |
- Changed constructor generation to avoid confusing IoC containers | |
- Fixed issue with empty Content folder | |
- Fixed issue with abstract controller base classes | |
2.1.00 (06-29-2009): | |
- Added Html.BeginForm overloads that use the strongly typed pattern | |
- Added Url() helpers on static resources to increase flexibility | |
- Changed generated constants (view and action names, static files) to be readonly strings | |
- Fixed null ref exception in Solution Folder logic | |
2.0.04 (06-28-2009): | |
- Fixed issue with files and folders with names starting with a digit | |
2.0.03 (06-27-2009): | |
- Rework code element enumeration logic to work around a VS2010 issue. The template should now work with VS2010 beta 1! | |
- Reduced some redundancy in the generated code | |
2.0.02 (06-27-2009): | |
- Added ActionLink overloads that take object instead of dictionary (from both Html and Ajax) | |
2.0.01 (06-26-2009): | |
- Fixed issue with files and folders with invalid identifier characters (e.g. spaces, '-', '.') | |
2.0.00 (06-26-2009): as described in http://blogs.msdn.com/davidebb/archive/2009/06/26/the-mvc-t4-template-is-now-up-on-codeplex-and-it-does-change-your-code-a-bit.aspx | |
- Added support for refactoring in Action methods | |
- The T4 file automatically runs whenever you build, instead of being done manually | |
- Support for strongly typed links to static resources | |
- Fix: supports controllers that are in sub-folders of the Controllers folder and not directly in there | |
- Fix: works better with nested solution folder | |
- Random other small fixes | |
1.0.xx (06-17-2009): the original based on this post | |
http://blogs.msdn.com/davidebb/archive/2009/06/17/a-new-and-improved-asp-net-mvc-t4-template.aspx | |
*/ | |
#> | |
<#@ template language="C#v3.5" debug="true" hostspecific="true" #> | |
<#@ assembly name="System.Core" #> | |
<#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #> | |
<#@ assembly name="EnvDTE" #> | |
<#@ assembly name="EnvDTE80" #> | |
<#@ import namespace="System.Collections.Generic" #> | |
<#@ import namespace="System.IO" #> | |
<#@ import namespace="System.Linq" #> | |
<#@ import namespace="System.Text.RegularExpressions" #> | |
<#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #> | |
<#@ import namespace="EnvDTE" #> | |
<#@ import namespace="EnvDTE80" #> | |
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #> | |
<# PrepareDataToRender(this); #> | |
// <auto-generated /> | |
// This file was generated by a T4 template. | |
// Don't change it directly as your change would get overwritten. Instead, make changes | |
// to the .tt file (i.e. the T4 template) and save it to regenerate this file. | |
#region T4MVC Generated Code | |
using System; | |
using System.Collections.Generic; | |
using System.Runtime.CompilerServices; | |
using System.Web; | |
using System.Web.Mvc; | |
using System.Web.Mvc.Ajax; | |
using System.Web.Mvc.Html; | |
using System.Web.Routing; | |
using <#= T4MVCNamespace #>; | |
[CompilerGenerated] | |
public static class <#= HelpersPrefix #> { | |
<# foreach (var controller in GetControllers()) { #> | |
public static <#= controller.FullClassName #> <#= controller.Name #> = new <#= controller.DerivedClassName #>(); | |
<# } #> | |
} | |
<# foreach (var controller in GetAbstractControllers().Where(c => !c.HasDefaultConstructor)) { #> | |
namespace <#= controller.Namespace #> { | |
public partial class <#= controller.ClassName #> { | |
protected <#= controller.ClassName #>() { } | |
} | |
} | |
<# } #> | |
<# foreach (var controller in GetControllers()) { #> | |
namespace <#= controller.Namespace #> { | |
public <# if (!controller.SharedViewFolder) { #>partial <# } #>class <#= controller.ClassName #> { | |
<# if (!controller.SharedViewFolder) { #> | |
<# if (!controller.HasExplicitConstructor) { #> | |
public <#= controller.ClassName #>() { } | |
<# } #> | |
[CLSCompliant(false)] | |
[CompilerGenerated] | |
protected <#= controller.ClassName #>(_Dummy d) { } | |
protected RedirectToRouteResult RedirectToAction(ActionResult result) { | |
var callInfo = (IT4MVCActionResult)result; | |
return RedirectToRoute(callInfo.RouteValues); | |
} | |
<# foreach (var method in controller.ActionMethodsUniqueWithoutParameterlessOverload) { #> | |
[NonAction] | |
public ActionResult <#= method.Name #>() { | |
return new T4MVC_ActionResult(Name, Actions.<#= method.ActionName #>); | |
} | |
<# } #> | |
[CompilerGenerated] | |
public readonly string Name = "<#= controller.Name #>"; | |
static readonly _Actions s_actions = new _Actions(); | |
[CLSCompliant(false)] | |
[CompilerGenerated] | |
public _Actions Actions { get { return s_actions; } } | |
[CLSCompliant(false)] | |
[CompilerGenerated] | |
public class _Actions { | |
<# foreach (var method in controller.ActionMethodsWithUniqueNames) { #> | |
public readonly string <#= method.ActionName #> = <#= method.ActionNameValueExpression #>; | |
<# } #> | |
} | |
<# } #> | |
static readonly _Views s_views = new _Views(); | |
[CLSCompliant(false)] | |
[CompilerGenerated] | |
public _Views Views { get { return s_views; } } | |
[CLSCompliant(false)] | |
[CompilerGenerated] | |
public class _Views { | |
<# RenderControllerViews(controller);#> | |
} | |
} | |
} | |
<# } #> | |
namespace <#= T4MVCNamespace #> { | |
<# foreach (var controller in GetControllers().Where(c => !c.SharedViewFolder)) { #> | |
[CompilerGenerated] | |
public class <#= controller.DerivedClassName #>: <#= controller.FullClassName #> { | |
public <#= controller.DerivedClassName #>() : base(_Dummy.Instance) { } | |
<# foreach (var method in controller.ActionMethods) { #> | |
public override <#= method.ReturnType #> <#= method.Name #>(<# method.WriteFormalParameters(true); #>) { | |
var callInfo = new T4MVC_<#= method.ReturnType #>("<#= controller.Name #>", Actions.<#= method.ActionName #>); | |
<# if (method.Parameters.Count > 0) { #> | |
<# foreach (var p in method.Parameters) { #> | |
callInfo.RouteValues.Add("<#= p.Name #>", <#= p.Name #>); | |
<# } #> | |
<# }#> | |
return callInfo; | |
} | |
<# } #> | |
} | |
<# } #> | |
[CLSCompliant(false)] | |
[CompilerGenerated] | |
public class _Dummy { | |
private _Dummy() { } | |
public static _Dummy Instance = new _Dummy(); | |
} | |
} | |
namespace System.Web.Mvc { | |
[CompilerGenerated] | |
public static class T4Extensions { | |
public static string ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result) { | |
return htmlHelper.RouteLink(linkText, result.GetRouteValueDictionary()); | |
} | |
public static string ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, object htmlAttributes) { | |
return ActionLink(htmlHelper, linkText, result, new RouteValueDictionary(htmlAttributes)); | |
} | |
public static string ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, IDictionary<string, object> htmlAttributes) { | |
return htmlHelper.RouteLink(linkText, result.GetRouteValueDictionary(), htmlAttributes); | |
} | |
public static MvcForm BeginForm(this HtmlHelper htmlHelper, ActionResult result, FormMethod formMethod) { | |
return htmlHelper.BeginForm(result, formMethod, null); | |
} | |
public static MvcForm BeginForm(this HtmlHelper htmlHelper, ActionResult result, FormMethod formMethod, object htmlAttributes) { | |
return BeginForm(htmlHelper, result, formMethod, new RouteValueDictionary(htmlAttributes)); | |
} | |
public static MvcForm BeginForm(this HtmlHelper htmlHelper, ActionResult result, FormMethod formMethod, IDictionary<string, object> htmlAttributes) { | |
var callInfo = (IT4MVCActionResult)result; | |
return htmlHelper.BeginForm(callInfo.Action, callInfo.Controller, callInfo.RouteValues, formMethod, htmlAttributes); | |
} | |
public static string Action(this UrlHelper urlHelper, ActionResult result) { | |
return urlHelper.RouteUrl(result.GetRouteValueDictionary()); | |
} | |
public static string ActionLink(this AjaxHelper ajaxHelper, string linkText, ActionResult result, AjaxOptions ajaxOptions) { | |
return ajaxHelper.RouteLink(linkText, result.GetRouteValueDictionary(), ajaxOptions); | |
} | |
public static string ActionLink(this AjaxHelper ajaxHelper, string linkText, ActionResult result, AjaxOptions ajaxOptions, object htmlAttributes) { | |
return ajaxHelper.RouteLink(linkText, result.GetRouteValueDictionary(), ajaxOptions, new RouteValueDictionary(htmlAttributes)); | |
} | |
public static string ActionLink(this AjaxHelper ajaxHelper, string linkText, ActionResult result, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) { | |
return ajaxHelper.RouteLink(linkText, result.GetRouteValueDictionary(), ajaxOptions, htmlAttributes); | |
} | |
public static Route MapRoute(this RouteCollection routes, string name, string url, ActionResult result) { | |
return routes.MapRoute(name, url, result, (ActionResult)null); | |
} | |
public static Route MapRoute(this RouteCollection routes, string name, string url, ActionResult result, object defaults) { | |
// Start by adding the default values from the anonymous object (if any) | |
var routeValues = new RouteValueDictionary(defaults); | |
// Then add the Controller/Action names and the parameters from the call | |
foreach (var pair in result.GetRouteValueDictionary()) { | |
routeValues.Add(pair.Key, pair.Value); | |
} | |
// Create and add the route | |
var route = new Route(url, routeValues, new MvcRouteHandler()); | |
routes.Add(name, route); | |
return route; | |
} | |
public static RouteValueDictionary GetRouteValueDictionary(this ActionResult result) { | |
return ((IT4MVCActionResult)result).RouteValues; | |
} | |
public static void InitMVCT4Result(this IT4MVCActionResult result, string controller, string action) { | |
result.Controller = controller; | |
result.Action = action; ; | |
result.RouteValues = new RouteValueDictionary(); | |
result.RouteValues.Add("Controller", controller); | |
result.RouteValues.Add("Action", action); | |
} | |
} | |
} | |
[CompilerGenerated] | |
public interface IT4MVCActionResult { | |
string Action { get; set; } | |
string Controller { get; set; } | |
RouteValueDictionary RouteValues { get; set; } | |
} | |
<# foreach (var resultType in ResultTypes.Values) { #> | |
[CompilerGenerated] | |
public class T4MVC_<#= resultType.Name #> : <#= resultType.Name #>, IT4MVCActionResult { | |
public T4MVC_<#= resultType.Name #>(string controller, string action): base(<# resultType.Constructor.WriteNonEmptyParameterValues(true); #>) { | |
this.InitMVCT4Result(controller, action); | |
} | |
<# foreach (var method in resultType.AbstractMethods) { #> | |
<#= method.IsPublic ? "public" : "protected" #> override void <#= method.Name #>(<# method.WriteFormalParameters(true); #>) { } | |
<# } #> | |
public string Controller { get; set; } | |
public string Action { get; set; } | |
public RouteValueDictionary RouteValues { get; set; } | |
} | |
<# } #> | |
namespace Links { | |
<# | |
foreach (string folder in StaticFilesFolders) { | |
ProcessStaticFiles(Project, folder); | |
} | |
#> | |
} | |
#endregion T4MVC | |
<#@ Include File="T4MVC.settings.t4" #> | |
<#+ | |
const string T4MVCNamespace = "T4MVC"; | |
const string ControllerSuffix = "Controller"; | |
static DTE Dte; | |
static Project Project; | |
static HashSet<ControllerInfo> Controllers; | |
static Dictionary<string, ResultTypeInfo> ResultTypes; | |
static TextTransformation TT; | |
static string T4FileName; | |
static IEnumerable<ControllerInfo> GetControllers() { | |
return Controllers.Where(c => !c.IsAbstract); | |
} | |
static IEnumerable<ControllerInfo> GetAbstractControllers() { | |
return Controllers.Where(c => c.IsAbstract); | |
} | |
void PrepareDataToRender(TextTransformation tt) { | |
TT = tt; | |
T4FileName = Path.GetFileName(Host.TemplateFile); | |
Controllers = new HashSet<ControllerInfo>(); | |
ResultTypes = new Dictionary<string, ResultTypeInfo>(); | |
// Get the DTE service from the host | |
Dte = (DTE)((IServiceProvider)Host).GetService(typeof(SDTE)); | |
Project = GetProjectContainingT4File(Dte); | |
if (Project == null) { | |
Error("Could not find the VS Project containing the T4 file."); | |
return; | |
} | |
ProcessControllersFolder(Project); | |
ProcessAllViews(Project); | |
} | |
Project GetProjectContainingT4File(DTE dte) { | |
// Just locating the Project that contains this T4 file is immensely difficult. | |
// If there is an easier way to do it, I'd love to know! | |
foreach (Project project in dte.Solution.Projects) { | |
Project foundProject = GetProjectContainingT4File(project); | |
if (foundProject != null) | |
return foundProject; | |
} | |
return null; | |
} | |
Project GetProjectContainingT4File(Project project) { | |
if (project.ConfigurationManager != null) { | |
// It's a Project | |
// Get the folder the project is in, making sure it ends with '\' | |
string projectFolder = Path.GetDirectoryName(project.FileName); | |
if (!projectFolder.EndsWith("\\")) | |
projectFolder += '\\'; | |
// If the .tt file is not under this folder, skip the project | |
if (!Host.TemplateFile.StartsWith(projectFolder, StringComparison.OrdinalIgnoreCase)) | |
return null; | |
// Get the relative path to the .tt file inside the project | |
string t4SubPath = Host.TemplateFile.Substring(projectFolder.Length); | |
// If it has the T4 file we're looking for, it's the one we want | |
ProjectItem projectItem = GetProjectItem(project, t4SubPath); | |
if (projectItem != null) { | |
// If the .tt file is not opened, open it | |
if (projectItem.Document == null) | |
projectItem.Open(Constants.vsViewKindCode); | |
if (AlwaysKeepTemplateDirty) { | |
// Mark the .tt file as unsaved. This way it will be saved and update itself next time the | |
// project is built. Basically, it keeps marking itself as unsaved to make the next build work. | |
// Note: this is certainly hacky, but is the best I could come up with so far. | |
projectItem.Document.Saved = false; | |
} | |
return project; | |
} | |
} | |
else if (project.ProjectItems != null) { | |
// It may be a solution folder. Need to recurse. | |
foreach (ProjectItem item in project.ProjectItems) { | |
if (item.SubProject == null) | |
continue; | |
Project foundProject = GetProjectContainingT4File(item.SubProject); | |
if (foundProject != null) | |
return foundProject; | |
} | |
} | |
return null; | |
} | |
void ProcessControllersFolder(Project project) { | |
// Get the Controllers folder | |
ProjectItem controllerProjectItem = GetProjectItem(project, ControllersFolder); | |
if (controllerProjectItem == null) | |
return; | |
ProcessControllersRecursive(controllerProjectItem); | |
} | |
void ProcessControllersRecursive(ProjectItem projectItem) { | |
// Recurse into all the sub-items (both files and folder can have some - e.g. .tt files) | |
foreach (ProjectItem item in projectItem.ProjectItems) { | |
ProcessControllersRecursive(item); | |
} | |
if (projectItem.FileCodeModel != null) { | |
// Process all the elements that are namespaces | |
foreach (CodeNamespace ns in GetNamespaces(projectItem.FileCodeModel.CodeElements)) { | |
ProcessControllerTypesInNamespace(ns); | |
} | |
} | |
} | |
void ProcessControllerTypesInNamespace(CodeNamespace ns) { | |
foreach (CodeClass2 type in GetClasses(ns.Members)) { | |
// Only process types that end with Controller | |
// REVIEW: this check is not super reliable. Should look at base class. | |
if (!type.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase)) | |
continue; | |
// Don't process generic classes (their concrete derived classes will be processed) | |
if (type.IsGeneric) | |
continue; | |
// Make sure the class is partial | |
if (type.ClassKind != vsCMClassKind.vsCMClassKindPartialClass) { | |
try { | |
type.ClassKind = vsCMClassKind.vsCMClassKindPartialClass; | |
} | |
catch { | |
// If we couldn't make it partial, give a warning and skip it | |
Warning(String.Format("{0} was not able to make the class {1} partial. Please change it manually if possible", T4FileName, type.Name)); | |
continue; | |
} | |
Warning(String.Format("{0} changed the class {1} to be partial", T4FileName, type.Name)); | |
} | |
// Collect misc info about the controller class and add it to the collection | |
var controllerInfo = new ControllerInfo() { | |
Namespace = ns.Name, | |
ClassName = type.Name | |
}; | |
// Either process new ControllerInfo or integrate results into existing object for partially defined controllers | |
var target = Controllers.Add(controllerInfo) ? controllerInfo : Controllers.First(c => c.Equals(controllerInfo)); | |
target.HasExplicitConstructor |= HasExplicitConstructor(type); | |
target.HasExplicitDefaultConstructor |= HasExplicitDefaultConstructor(type); | |
if (type.IsAbstract) { | |
// If it's abstract, set a flag and don't process action methods (derived classes will) | |
target.IsAbstract = true; | |
} | |
else { | |
// Process all the action methods in the controller | |
ProcessControllerActionMethods(target, type); | |
} | |
} | |
} | |
void ProcessControllerActionMethods(ControllerInfo controllerInfo, CodeClass2 current) { | |
// We want to process not just the controller class itself, but also its parents, as they | |
// may themselves define actions | |
for (CodeClass2 type = current; type != null && type.FullName != "System.Web.Mvc.Controller"; type = (CodeClass2)type.Bases.Item(1)) { | |
// If the type doesn't come from this project, some actions on it will fail. Try to get a real project type if possible. | |
if (type.InfoLocation != vsCMInfoLocation.vsCMInfoLocationProject) { | |
// Go through all the projects in the solution | |
foreach (Project prj in Dte.Solution.Projects) { | |
// Skip it if it's the current project or doesn't have a code model | |
if (prj == Project || prj.CodeModel == null) | |
continue; | |
// If we can get a local project type, use it instead of the original | |
var codeType = prj.CodeModel.CodeTypeFromFullName(type.FullName); | |
if (codeType != null && codeType.InfoLocation == vsCMInfoLocation.vsCMInfoLocationProject) { | |
type = (CodeClass2)codeType; | |
break; | |
} | |
} | |
} | |
foreach (CodeFunction2 method in GetMethods(type)) { | |
// Ignore non-public methods | |
if (method.Access != vsCMAccess.vsCMAccessPublic) | |
continue; | |
// This takes care of avoiding generic types which cause method.Type.CodeType to blow up | |
if (method.Type.TypeKind != vsCMTypeRef.vsCMTypeRefCodeType) | |
continue; | |
// We only support action methods that return an ActionResult derived type | |
if (!method.Type.CodeType.get_IsDerivedFrom("System.Web.Mvc.ActionResult")) { | |
Warning(String.Format("{0} doesn't support {1}.{2} because it doesn't return a supported ActionResult type", T4FileName, type.Name, method.Name)); | |
continue; | |
} | |
// If we haven't yet seen this return type, keep track of it | |
if (!ResultTypes.ContainsKey(method.Type.CodeType.Name)) { | |
var resTypeInfo = new ResultTypeInfo(method.Type.CodeType); | |
ResultTypes[method.Type.CodeType.Name] = resTypeInfo; | |
} | |
// Make sure the method is virtual | |
if (!method.CanOverride) { | |
try { | |
method.CanOverride = true; | |
} | |
catch { | |
// If we couldn't make it virtual, give a warning and skip it | |
Warning(String.Format("{0} was not able to make the action method {1}.{2} virtual. Please change it manually if possible", T4FileName, type.Name, method.Name)); | |
continue; | |
} | |
Warning(String.Format("{0} changed the action method {1}.{2} to be virtual", T4FileName, type.Name, method.Name)); | |
} | |
// Collect misc info about the action method and add it to the collection | |
controllerInfo.ActionMethods.Add(new ActionMethodInfo(method, controllerInfo)); | |
} | |
} | |
} | |
void ProcessAllViews(Project project) { | |
// Get the Views folder | |
ProjectItem viewsProjectItem = GetProjectItem(project, ViewsRootFolder); | |
if (viewsProjectItem == null) | |
return; | |
// Go through all the sub-folders in the Views folder | |
foreach (ProjectItem item in viewsProjectItem.ProjectItems) { | |
// We only care about sub-folders, not files | |
if (!IsFolder(item)) | |
continue; | |
ControllerInfo controller; | |
// Treat Shared as a pseudo-controller for consistency | |
if (item.Name.Equals("Shared", StringComparison.OrdinalIgnoreCase)) { | |
controller = new ControllerInfo() { SharedViewFolder = true, Namespace = T4MVCNamespace, ClassName = "Shared" + ControllerSuffix }; | |
Controllers.Add(controller); | |
} | |
else { | |
// Find the controller for this view folder | |
controller = Controllers.SingleOrDefault(c => c.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase)); | |
if (controller == null) { | |
Error(String.Format("The Views folder has a sub-folder named '{0}', but there is no matching controller", item.Name)); | |
continue; | |
} | |
} | |
AddViewsRecursive(String.Empty, item.ProjectItems, controller.ViewsFolder); | |
} | |
} | |
void AddViewsRecursive(string prefix, ProjectItems items, ViewsFolderInfo viewsFolder) { | |
// Go through all the files in the subfolder to get the view names | |
foreach (ProjectItem item in items) { | |
string viewName = Path.GetFileNameWithoutExtension(item.Name); | |
if (item.Kind == Constants.vsProjectItemKindPhysicalFile) { | |
if (Path.GetExtension(item.Name).Equals(".master", StringComparison.OrdinalIgnoreCase)) | |
continue; // ignore master files | |
viewsFolder.Views.Add(viewName); | |
} | |
else if (item.Kind == Constants.vsProjectItemKindPhysicalFolder) { | |
var subViewFolder = new ViewsFolderInfo() { FullName = prefix + viewName }; | |
if (subViewFolder.Name.Equals("App_LocalResources", StringComparison.OrdinalIgnoreCase)) | |
continue; | |
viewsFolder.SubFolders.Add(subViewFolder); | |
AddViewsRecursive(prefix + viewName + "/", item.ProjectItems, subViewFolder); | |
} | |
} | |
} | |
void RenderControllerViews(ControllerInfo controller) { | |
PushIndent(" "); | |
RenderViewsRecursive(controller.ViewsFolder); | |
PopIndent(); | |
} | |
void RenderViewsRecursive(ViewsFolderInfo viewsFolder) { | |
// For each view, generate a readonly string | |
foreach (string view in viewsFolder.Views) { | |
WriteLine("public readonly string " + Sanitize(view) + " = \"" + viewsFolder.GetFullViewName(view) + "\";"); | |
} | |
// For each sub folder, generate a class and recurse | |
foreach (var subFolder in viewsFolder.SubFolders) { | |
string newClassName = Sanitize(subFolder.Name);#> | |
static readonly _<#=newClassName#> s_<#=newClassName#> = new _<#=newClassName#>(); | |
public _<#=newClassName#> <#=newClassName#> { get { return s_<#=newClassName#>; } } | |
public partial class _<#=newClassName#>{ | |
<#+ | |
PushIndent(" "); | |
RenderViewsRecursive(subFolder); | |
PopIndent(); | |
WriteLine("}"); | |
} | |
} | |
void ProcessStaticFiles(Project project, string folder) { | |
ProjectItem folderProjectItem = GetProjectItem(project, folder); | |
if (folderProjectItem != null) { | |
ProcessStaticFilesRecursive(folderProjectItem, "~"); | |
} | |
} | |
void ProcessStaticFilesRecursive(ProjectItem projectItem, string path) { | |
if (IsFolder(projectItem)) { #> | |
[CompilerGenerated] | |
public static class @<#=Sanitize(projectItem.Name) #> { | |
public static string Url() { return VirtualPathUtility.ToAbsolute("<#=path#>/<#=projectItem.Name#>"); } | |
public static string Url(string fileName) { return VirtualPathUtility.ToAbsolute("<#=path#>/<#=projectItem.Name#>/" + fileName); } | |
<#+ | |
PushIndent(" "); | |
// Recurse into all the items in the folder | |
foreach (ProjectItem item in projectItem.ProjectItems) { | |
ProcessStaticFilesRecursive(item, path + "/" + projectItem.Name); | |
} | |
PopIndent(); | |
#> | |
} | |
<#+ | |
} | |
else { #> | |
public static readonly string <#=Sanitize(projectItem.Name)#> = Url("<#=projectItem.Name#>"); | |
<#+ | |
// Non folder items may also have children (virtual folders, Class.cs -> Class.Designer.cs, template output) | |
// Just register them on the same path as their parent item | |
foreach (ProjectItem item in projectItem.ProjectItems) { | |
ProcessStaticFilesRecursive(item, path); | |
} | |
} | |
} | |
ProjectItem GetProjectItem(Project project, string name) { | |
return GetProjectItem(project.ProjectItems, name); | |
} | |
ProjectItem GetProjectItem(ProjectItems items, string subPath) { | |
ProjectItem current = null; | |
foreach (string name in subPath.Split('\\')) { | |
try { | |
// ProjectItems.Item() throws when it doesn't exist, so catch the exception | |
// to return null instead. | |
current = items.Item(name); | |
} | |
catch { | |
// If any chunk couldn't be found, fail | |
return null; | |
} | |
items = current.ProjectItems; | |
} | |
return current; | |
} | |
// Return all the CodeNamespaces in the CodeElements collection | |
static IEnumerable<CodeNamespace> GetNamespaces(CodeElements codeElements) { | |
return GetElements<CodeNamespace>(codeElements); | |
} | |
// Return all the CodeClass2 in the CodeElements collection | |
static IEnumerable<CodeClass2> GetClasses(CodeElements codeElements) { | |
return GetElements<CodeClass2>(codeElements); | |
} | |
// Return all the CodeFunction2 in the CodeElements collection | |
static IEnumerable<CodeFunction2> GetMethods(CodeClass2 codeClass) { | |
// Only look at regular method (e.g. ignore things like contructors) | |
return GetElements<CodeFunction2>(codeClass.Members) | |
.Where(f => f.FunctionKind == vsCMFunction.vsCMFunctionFunction); | |
} | |
// Check if the class has any explicit constructor | |
static bool HasExplicitConstructor(CodeClass2 codeClass) { | |
return GetElements<CodeFunction2>(codeClass.Members).Any( | |
f => f.FunctionKind == vsCMFunction.vsCMFunctionConstructor); | |
} | |
// Check if the class has a default (i.e. no params) constructor | |
static bool HasExplicitDefaultConstructor(CodeClass2 codeClass) { | |
return GetElements<CodeFunction2>(codeClass.Members).Any( | |
f => f.FunctionKind == vsCMFunction.vsCMFunctionConstructor && f.Parameters.Count == 0); | |
} | |
// Find a method with a given name | |
static CodeFunction2 GetMethod(CodeClass2 codeClass, string name) { | |
return GetMethods(codeClass).FirstOrDefault(f => f.Name == name); | |
} | |
static IEnumerable<T> GetElements<T>(CodeElements codeElements) where T : class { | |
// Note: this code can be simplified to just: | |
// return codeElements.OfType<T>(); | |
// But this breaks on VS2010 beta due to a VS bug (that should be fixed in VS2010 beta 2). | |
// For now, work around using this alternate code which avoids getting the enumerator | |
for (int i = 1; i <= codeElements.Count; i++) { | |
var codeNamespace = codeElements.Item(i) as T; | |
if (codeNamespace != null) | |
yield return codeNamespace; | |
} | |
} | |
// Return whether a ProjectItem is a folder and not a file | |
static bool IsFolder(ProjectItem item) { | |
return (item.Kind == Constants.vsProjectItemKindPhysicalFolder); | |
} | |
static string Sanitize(string token) { | |
// Replace all invalid chars by underscores | |
token = Regex.Replace(token, @"[\W\b]", "_", RegexOptions.IgnoreCase); | |
// If it starts with a digit, prefix it with an underscore | |
token = Regex.Replace(token, @"^\d", @"_$0"); | |
// Check for reserved words | |
// TODO: Clean this up and add other reserved words (keywords, etc) | |
if (token == "Url") token = "_Url"; | |
return token; | |
} | |
// Data structure to collect data about a controller class | |
class ControllerInfo { | |
public ControllerInfo() { | |
ActionMethods = new HashSet<ActionMethodInfo>(); | |
ViewsFolder = new ViewsFolderInfo(); | |
} | |
// True when this is not a real controller, but a placeholder for the Shared views folder | |
public bool SharedViewFolder { get; set; } | |
public bool HasExplicitConstructor { get; set; } | |
public bool HasExplicitDefaultConstructor { get; set; } | |
public bool HasDefaultConstructor { get { return !HasExplicitConstructor || HasExplicitDefaultConstructor; } } | |
public bool IsAbstract { get; set; } | |
public string ClassName { get; set; } | |
public string Name { | |
get { | |
// Trim the Controller suffix | |
return ClassName.Substring(0, ClassName.Length - ControllerSuffix.Length); | |
} | |
} | |
public string Namespace { get; set; } | |
public string FullClassName { | |
get { | |
return Namespace + "." + ClassName; | |
} | |
} | |
public string DerivedClassName { | |
get { | |
if (SharedViewFolder) | |
return FullClassName; | |
return "T4MVC_" + ClassName; | |
} | |
} | |
public HashSet<ActionMethodInfo> ActionMethods { get; set; } | |
IEnumerable<ActionMethodInfo> ActionMethodsWithNoParameters { | |
get { | |
return ActionMethods.Where(m => m.Parameters.Count == 0); | |
} | |
} | |
public IEnumerable<ActionMethodInfo> ActionMethodsUniqueWithoutParameterlessOverload { | |
get { | |
return ActionMethodsWithUniqueNames.Except(ActionMethodsWithNoParameters, new ActionComparer()); | |
} | |
} | |
// Return a list of actions without duplicate names (even with multiple overloads) | |
public IEnumerable<ActionMethodInfo> ActionMethodsWithUniqueNames { | |
get { | |
return ActionMethods.Distinct(new ActionComparer()); | |
} | |
} | |
class ActionComparer : IEqualityComparer<ActionMethodInfo> { | |
public bool Equals(ActionMethodInfo x, ActionMethodInfo y) { | |
return x.ActionName == y.ActionName; | |
} | |
public int GetHashCode(ActionMethodInfo obj) { | |
return obj.ActionName.GetHashCode(); | |
} | |
} | |
public ViewsFolderInfo ViewsFolder { get; private set; } | |
public override string ToString() { | |
return Name; | |
} | |
public override bool Equals(object obj) { | |
return obj != null && FullClassName == ((ControllerInfo)obj).FullClassName; | |
} | |
public override int GetHashCode() { | |
return FullClassName.GetHashCode(); | |
} | |
} | |
// Info about a view folder, its views and its sub view folders | |
class ViewsFolderInfo { | |
public ViewsFolderInfo() { | |
Views = new HashSet<string>(); | |
SubFolders = new List<ViewsFolderInfo>(); | |
} | |
public string FullName { get; set; } | |
public string Name { | |
get { | |
return FullName.Substring(FullName.LastIndexOf("/") + 1); | |
} | |
} | |
public HashSet<string> Views { get; private set; } | |
public List<ViewsFolderInfo> SubFolders { get; set; } | |
public string GetFullViewName(string viewName) { | |
if (String.IsNullOrEmpty(FullName)) | |
return viewName; | |
return FullName + "/" + viewName; | |
} | |
} | |
// Data structure to collect data about a method | |
class FunctionInfo { | |
protected CodeFunction2 _method; | |
private string _signature; | |
public FunctionInfo(CodeFunction2 method) { | |
_method = method; | |
// Build a unique signature for the method, used to avoid duplication | |
_signature = method.Name; | |
// Process all the parameters | |
Parameters = new List<MethodParamInfo>(); | |
foreach (var p in GetElements<CodeParameter2>(method.Parameters)) { | |
Parameters.Add( | |
new MethodParamInfo() { | |
Name = p.Name, | |
Type = p.Type.AsString | |
}); | |
_signature += "," + p.Type.AsString; | |
} | |
} | |
public string Name { get { return _method.Name; } } | |
public string ReturnType { get { return _method.Type.CodeType.Name; } } | |
public bool IsPublic { get { return _method.Access == vsCMAccess.vsCMAccessPublic; } } | |
public List<MethodParamInfo> Parameters { get; private set; } | |
// Write out all the parameters as part of a method declaration | |
public void WriteFormalParameters(bool first) { | |
foreach (var p in Parameters) { | |
if (first) | |
first = false; | |
else | |
TT.Write(", "); | |
TT.Write(p.Type + " " + p.Name); | |
} | |
} | |
// Pass non-empty param values to make sure the ActionResult ctors don't complain | |
// REVIEW: this is a bit dirty | |
public void WriteNonEmptyParameterValues(bool first) { | |
foreach (var p in Parameters) { | |
if (first) | |
first = false; | |
else | |
TT.Write(", "); | |
switch (p.Type) { | |
case "string": | |
TT.Write("\" \""); | |
break; | |
case "byte[]": | |
TT.Write("new byte[0]"); | |
break; | |
default: | |
TT.Write("null"); | |
break; | |
} | |
} | |
} | |
public override bool Equals(object obj) { | |
return obj != null && _signature == ((FunctionInfo)obj)._signature; | |
} | |
public override int GetHashCode() { | |
return _signature.GetHashCode(); | |
} | |
} | |
// Data structure to collect data about an action method | |
class ActionMethodInfo: FunctionInfo { | |
public ActionMethodInfo(CodeFunction2 method, ControllerInfo controller): base(method) { | |
// Normally, the action name is the method name. But if there is an [ActionName] on | |
// the method, get the expression from that instead | |
ActionNameValueExpression = '"' + Name + '"'; | |
for (int i = 1; i <= method.Attributes.Count; i++) { | |
var attrib = (CodeAttribute2)method.Attributes.Item(i); | |
if (attrib.FullName == "System.Web.Mvc.ActionNameAttribute") { | |
var arg = (CodeAttributeArgument)attrib.Arguments.Item(1); | |
ActionNameValueExpression = arg.Value; | |
} | |
} | |
} | |
public string ActionName { get { return Name; } } | |
public string ActionNameValueExpression { get; set; } | |
} | |
// Data about an ActionResult derived type | |
class ResultTypeInfo { | |
CodeType _codeType; | |
public ResultTypeInfo(CodeType codeType) { | |
_codeType = codeType; | |
var ctor = GetElements<CodeFunction2>(_codeType.Members).FirstOrDefault( | |
f => f.FunctionKind == vsCMFunction.vsCMFunctionConstructor); | |
Constructor = new FunctionInfo(ctor); | |
} | |
public string Name { get { return _codeType.Name; } } | |
public FunctionInfo Constructor { get; set; } | |
public IEnumerable<FunctionInfo> AbstractMethods { | |
get { | |
return GetElements<CodeFunction2>(_codeType.Members).Where( | |
f => f.MustImplement).Select(f => new FunctionInfo(f)); | |
} | |
} | |
} | |
class MethodParamInfo { | |
public string Name { get; set; } | |
public string Type { get; set; } | |
} | |
#> |
Labels: asp.net, asp.net mvc, errors, howto, mvc, t4, visual studio
Monday, August 24, 2009
Web Setup MSI woes on IIS7 + solution
Just a note to my future self and to anyone else who might stumble on this:
We created an MSI to install our MVC app, but the new test server refused to install it:
The Installer simply stopped, with an Installation interrupted message, and the application event log listed the following:
Windows Installer installed the product. Product Name: XXXXX. Product Version: x.y.z. Product Language: 1033. Installation success or error status: 1603.
The correct google search term here is: Installation success or error status: 1603.
It will lead you to the solution by Ben Noyce at NInitiative:
Long nights and story short, in order to install a web setup project on Windows Server 2008 and IIS 7, you need to install the IIS 6 Metabase Compatibility role service.
Thanks, Ben!
Labels: asp.net, asp.net mvc, errors, howto, microsoft, mvc, rant, rave
Wednesday, July 29, 2009
ASP.NET MVC: Corrected Moq based MvcMockHelpers
using System; | |
using System.Collections.Specialized; | |
using System.Web; | |
using System.Web.Mvc; | |
using System.Web.Routing; | |
using Moq; | |
namespace Mvc.Web.Tests | |
{ | |
public static class MvcMockHelpers | |
{ | |
public static HttpContextBase FakeHttpContext() | |
{ | |
var context = new Mock<HttpContextBase>(); | |
var request = new Mock<HttpRequestBase>(); | |
var response = new Mock<HttpResponseBase>(); | |
var session = new Mock<HttpSessionStateBase>(); | |
var server = new Mock<HttpServerUtilityBase>(); | |
context.Setup(ctx => ctx.Request).Returns(request.Object); | |
context.Setup(ctx => ctx.Response).Returns(response.Object); | |
context.Setup(ctx => ctx.Session).Returns(session.Object); | |
context.Setup(ctx => ctx.Server).Returns(server.Object); | |
return context.Object; | |
} | |
public static HttpContextBase FakeHttpContext(string url) | |
{ | |
HttpContextBase context = FakeHttpContext(); | |
context.Request.SetupRequestUrl(url); | |
return context; | |
} | |
public static void SetFakeControllerContext(this Controller controller) | |
{ | |
var httpContext = FakeHttpContext(); | |
ControllerContext context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller); | |
controller.ControllerContext = context; | |
} | |
static string GetUrlFileName(string url) | |
{ | |
if (url.Contains("?")) | |
return url.Substring(0, url.IndexOf("?")); | |
else | |
return url; | |
} | |
static NameValueCollection GetQueryStringParameters(string url) | |
{ | |
if (url.Contains("?")) | |
{ | |
NameValueCollection parameters = new NameValueCollection(); | |
string[] parts = url.Split("?".ToCharArray()); | |
string[] keys = parts[1].Split("&".ToCharArray()); | |
foreach (string key in keys) | |
{ | |
string[] part = key.Split("=".ToCharArray()); | |
parameters.Add(part[0], part[1]); | |
} | |
return parameters; | |
} | |
else | |
{ | |
return null; | |
} | |
} | |
public static void SetHttpMethodResult(this HttpRequestBase request, string httpMethod) | |
{ | |
Mock.Get(request).Setup(req => req.HttpMethod).Returns(httpMethod); | |
} | |
public static void SetupRequestUrl(this HttpRequestBase request, string url) | |
{ | |
if (url == null) | |
throw new ArgumentNullException("url"); | |
if (!url.StartsWith("~/")) | |
throw new ArgumentException("Sorry, we expect a virtual url starting with \"~/\"."); | |
var mock = Mock.Get(request); | |
mock.Setup(req => req.QueryString).Returns(GetQueryStringParameters(url)); | |
mock.Setup(req => req.AppRelativeCurrentExecutionFilePath).Returns(GetUrlFileName(url)); | |
mock.Setup(req => req.PathInfo).Returns(string.Empty); | |
} | |
} | |
} |
Labels: asp.net, asp.net mvc, errors, howto, mvc, programming, testing
Tuesday, June 16, 2009
Twitter Conversations: More fun with Yahoo Pipes
<Preface>
Lets imagine that you can’t actually do all of this directly in Twitter Search like this:
http://search.twitter.com/search?q=from:jaketapper+to:senjohnmccain+OR+from:senjohnmccain+to:jaketapper&since=2009-06-16&until=2009-06-16&rpp=50
</Preface>
(Now contrived) How To follows:
I stumbled upon an impromptu (or so they claim) twitter interview between Jake Tapper (@jaketapper) of ABC and Sen. John McCain (@senjohnmccain). Unfortunately, short of following both of them, it was hard to get a gist of the actual conversation – I tried the Conversation view on Twitter Search, and a couple of other services, like Tweader (appears broken) and Quotably (dead).
Twitter Search has an advanced search/allows parameters to specify searching for tweets from one person to another, but that only gives you half a conversation. And as far as I can tell, Twitter search doesn’t allow multiple from:/to: pairs. Enter Yahoo Pipes’ union module:
This module merges up to five separate sources of items into a single list of items.
Since Twitter Search results can be had in both Atom and RSS flavor, this means we’re good to go:
<interlude>
…and this is where I discovered that you could indeed do duplex conversation searches in twitter search, so the rest of this will be short…
</interlude>
See http://pipes.yahoo.com/austegard/twitterconversations; click View Source or Clone to play with it (requires Yahoo id).
Labels: errors, experiment, mashup, me-me-me, twitter, yahoo
Tuesday, June 09, 2009
Outlook's dumbest feature?
Why? Why? Why?
It seems 9 out of 10 times the line breaks are significant and SHOULDN’T be removed.
The feature has persisted for about a decade (if not longer – I don’t recall if it was part of 97 and 98); if you don’t know by now, this is how you remove it:
Tools – Options – Preferences – Email Options, Uncheck the Remove… checkbox.
Tuesday, June 02, 2009
Monday, June 01, 2009
Thursday, May 14, 2009
Help the internets are down?!
Seriously, how can someone as big and distributed as Google have network issues? (And when my blog is on Google, how can I gripe about it?)
This morning’s tracert (and confirmed via a quick twitter search):
C:\Users\oskar.austegard>tracert google.com Tracing route to google.com [209.85.171.100] over a maximum of 30 hops: 1 <1 ms <1 ms <1 ms Wireless_Broadband_Router.home [192.168.1.1] 2 6 ms 4 ms 4 ms xx.xxx.xx.xx 3 6 ms 5 ms 8 ms xxxxxxx.verizon-gni.net [130.81.xxx.xxx] 4 6 ms 8 ms 5 ms so-3-0-0-0.LCC1-RES-BB-RTR1-RE1.verizon-gni.net [130.81.29.0] 5 8 ms 5 ms 6 ms 0.so-1-2-0.XL3.IAD8.ALTER.NET [152.63.37.117] 6 12 ms 14 ms 7 ms 0.ge-6-0-0.BR2.IAD8.ALTER.NET [152.63.41.149] 7 9 ms 8 ms 9 ms te-11-3-0.edge1.Washington4.level3.net [4.68.63.169] 8 10 ms 16 ms 17 ms vlan69.csw1.Washington1.Level3.net [4.68.17.62] 9 11 ms 9 ms 10 ms ae-62-62.ebr2.Washington1.Level3.net [4.69.134.145] 10 28 ms 35 ms 28 ms ae-2-2.ebr2.Chicago2.Level3.net [4.69.132.69] 11 27 ms 27 ms 29 ms ae-1-100.ebr1.Chicago2.Level3.net [4.69.132.113] 12 54 ms 55 ms 54 ms ae-3.ebr2.Denver1.Level3.net [4.69.132.61] 13 103 ms 92 ms 104 ms ae-2.ebr2.Seattle1.Level3.net [4.69.132.53] 14 92 ms 92 ms 92 ms ae-21-52.car1.Seattle1.Level3.net [4.68.105.34] 15 * * * Request timed out. 16 * * * Request timed out. 17 * * * Request timed out. 18 369 ms * * 64.233.174.125 19 * * 285 ms 64.233.174.99 20 * * * Request timed out. 21 375 ms 347 ms * 74.125.30.134 22 * * * Request timed out. 23 * * 350 ms cg-in-f100.google.com [209.85.171.100] Trace complete.
Tuesday, April 28, 2009
Wrap-up: Amazon Payments responds. In English.
I finally received my first decent response from Amazon Payment Customer Support:
Amazon.com Customer Service to me
Hello again from Amazon Payments.
I apologize for our misunderstanding and delay in responding to your message.
We do have the ability to review your previous correspondence with us, and I see that you were notified April 25 that your account had been reinstated.
It appears that my colleague misunderstood your concern, and instead of recognizing your desire to simply have the account reinstated, he was checking into the specific transaction you were wanting to make. I'm sorry for the mix-up.
I'll also pass on your feedback concerning offering phone support.
Thank you for using Amazon Payments.
Please …. (standard footer followed)
Best regards,
JoEllen M.
Amazon.com
We're Building Earth's Most Customer-Centric Company
http://www.amazon.com/your-account
Thank you JoEllen M, for providing the very first email from Amazon Payment Customer Support that sounded like it was written by someone who had read the case history, and who was willing to take the time to write a non-form-letter reponse. If this had been done last week (or back in Oct/Nov when it first happened) you would have saved me and Amazon a lot of time (and bad blog-press).
Rant: Amazon Payments customer support still poor
I had to take a breath there, before I chose to go with the word “poor”. I was very tempted to go with an alliteration.
Got the following email from Amazon.Com Customer Service this morning, 3 days after the issue was resolved. Some numbers and addresses have been altered.
Greetings from Amazon.com.
Sometimes a credit card number will experience one or more failed attempts before a charge is ultimately successful. In this case, I've checked your Amazon Payments Business account affiliated with austxxxxx@gmail.com and found that your credit card was successfully charged on April 26, 2009, by Jungle Disk, Inc..
Transaction amount: $1.02
Transaction ID: 253FAROT22PMHJ3P5GJ1PABCDEF12345678
As always please feel free to contact us should you have future questions or comments. If you need to contact us back, you can do so by using the secure form at the following specialized link to ensure we receive your next message:
https://payments.amazon.com/sdui/sdui/contactussend
Thank you for sending us your question to Amazon Payments.
Please let us know if this e-mail resolved your question:
If yes, click here:
http://www.amazon.com/rsvp-y?c=wyeewufg5555555555
If not, click here:
http://www.amazon.com/rsvp-n?c=wyeewufg5555555555
Please note: this e-mail was sent from an address that cannot accept incoming e-mail.
To contact us about an unrelated issue, please visit the Help section of our web site.
Best regards,
Donny
Amazon.com
We're Building Earth's Most Customer-Centric Company
http://www.amazon.com/your-account
This was in response to my rather angry request from Friday – 4 days ago (my account id has been changed for my protection):
*** DO NOT REPLY TO THIS EMAIL WITH THE "We have taken this action because it has come to our attention that this account is related to a previously closed account." FORM LETTER ***
Please review previous correspondence for Customer ID: B37NK34KABCDEFGH
I am writing this from my amazonpayments account, which shows in the header as an active business account. I have no interest in this being a business account. All I want is SOME account tied to austexxxx@gmail.com that can process payments to JungleDisk.
If I try to access this same account through JungleDisk's Amazon interface (https://www.amazon.com/gp/aws/ssop/index.html?....) I am told that "Access to Your Account Is Temporarily Disabled".
Gee, thanks for checking, Donny, and for not using a form letter response. But if you had bothered looking into the case history you would see that your response was a) not at all correct and b) three days too late.
As I said before, Amazon Payments customer support needs the permission to deal with customers directly by phone to avoid wasting everybody's time.
PS! And in response to all the vendors who’re coming out of the woodwork suggesting their own alternatives to JungleDisk – this is not about JungleDisk – JungleDisk is great. I’m happy with JungleDisk. This is not even about Amazon (the store) which I am also happy with. This is about Amazon Payments Customer Support, which I am still unhappy with.