mo.notono.us

Thursday, October 11, 2012

Recent Items in Windows 8

I  just got Windows 8 installed on my work desktop a few days back, and am still trying to find my way around the place.  The jury is still out if this is a great OS for non-touch laptop/desktop users; I really appreciate some of the performance features; some of the UX, not so much.

This morning I was looking for a file I had just closed, and I couldn't remember the filename or the path.  Recent Items to the rescue, I thought. So I hit Windows, typed "Recent" - nothing - continued, "Recent Items" - nothing.  Hmm.  Hit Windows + F to do a file search only - still nada.

Google to the rescue, as always:

From elessarGObonzo: the folder is still located in "C:\Users\[yourusername]\AppData\Roaming\Microsoft\Windows" as 'Recent Items'

Unfortunately this folder is a special folder that can't simply be added as a Toolbar - that would have been great.  You can however add it to your Start menu, Desktop, and also to the Taskbar.

To add Recent items to your Start menu, simply right-click and select Pin to Start.  Done. (Tip: Hit Windows to bring up Start and then move the icon to a place you can find it.)

To add Recent Items to the Desktop is also easy, rght-click, select Send To > Desktop (create shortcut).  Done.

To add Recent Items to the Taskbar is more complex - you have to add a shortcut to the Taskbar folder, which like the Recent Items folder itself is hidden.

From Windows 7 Themes: the Taskbar folder is located here: C:\Users\[yourusername]\AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar

So simply Alt-drag the Recent Items from its folder to the Taskbar folder and you now have a button in the Desktop taskbar.

Not quite the same as Windows 7, but manageable.

Labels: , ,

Wednesday, November 16, 2011

A non-trendy way to wrap text to the width of an image

Say you have an image, whose width is unknown.  You want to display a caption below the image, and the length of the caption text is also unknown.  How do you display the caption so that the text wraps to the width of the image?

As far as I know, there is no way to do this with divs and Css.  It *may* be possible to do it with figures and figcaptions, but now you’re in Html5 land, and to support older browsers you’ll need to do gymnastics.

You could use an img load event handler and resize the caption after the image comes in, but now you have to add javascript for something that should be handled by your html.

So you use tables.  Yes tables, those horrible, horrible remnants of Web 1.0.

And you do it like this (though your styles would obviously be in a css stylesheet somewhere):

<table>
<caption style="caption-side: bottom; margin: 0 5px;">Oh caption, my caption! our fearful task is done!<br>
The layout has weathered every wrack, the prize we sought is won</caption>
<tr><td><img alt="some unknown sized image" src="http://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Ocaptain.jpg/394px-Ocaptain.jpg">
&/lt;tr></table>

which renders thusly (Live writer may corrupt this - sorry view-sourcers):

Oh caption, my caption! our fearful task is done!
The layout has weathered every wrack, the prize we sought is won
some unknown sized image

Also see http://jsfiddle.net/austegard/fGwve/

Labels: , , ,

Tuesday, September 13, 2011

Getting the schema of a dataset from a webservice

I found the following in my Blogger Drafts folder from two-thousand-and-freaking-five.  Since I had completely forgotten about this and never published it, I figured I'd do so now...

1/20/2005:
In a comment to : Evil = WebService DataSet; //Fix this, Gary McDonald mentions:
You are familiar with the auto-generated WSDL obtained by calling your ASMX with the ?wsdl request, so try ?schema=xxx where xxx is the name of the returned XSD.

For example, if I have an ASMX at http://www.tempuri.org/MyService.asmx and It has a Method named Foo that returns a typed dataset named Bar, I can get the schema from http://www.tempuri.org/MyService.asmx?schema=Bar
I wasn't aware of that. Cool.

Labels: , , , ,

Monday, August 15, 2011

Stop Word and Live Writer from Replacing "Straight Quotes" with “Smart Quotes”

Whenever I write any coding documentation, Word and Live Writer like to “help out” by auto correcting some stuff that I’d rather it left alone, since it tends to break code samples. Quotes are on top of that list – so here goes:

From the ICan’tBelieveIHaventDoneThisBefore department, here is how to stop Word from replacing "straight quotes" with “smart quotes”.

In Word, hit the Office Button, then Word Options, then the Proofing tab, then the AutoCorrect Options, then the AutoFormat As You Type tab, then uncheck "Straight quotes" with “smart quotes”.

image

(thanks to Herb Tyson who posted this here: http://help.lockergnome.com/office/prevent-Word-Outlook-Replaceing-Apostrophes--ftopict987047.html)

In Live Writer, it's a bit more straight-forward, simply select the (tiny) Live Writer button, then Options, then Editing, then uncheck Replace "straight quotes" with “smart quotes”

image

There – "that's better".

Labels: , , , ,

Tuesday, March 01, 2011

Practical example of jQuery 1.5’s deferred.when() and .then()

"“Fun with jQuery Templating and AJAX” by Dan Wellman is a generally interesting article, but I found the code in the “Getting the Data” block especially interesting – see how each get function RETURNS the $.ajax function call, which can then be called inside a when() function, vastly simplifying the workflow (there’s an error in the listed code – getTweets() is supposed to return the ajax function, not simply execute it).

http://net.tutsplus.com/tutorials/javascript-ajax/fun-with-jquery-templating-and-ajax/

Even more interesting is this pattern, suggested by commenter Eric Hynds (whose blog has now been added to my Google Reader list):

http://net.tutsplus.com/tutorials/javascript-ajax/fun-with-jquery-templating-and-ajax/comment-page-1/#comment-357637

$.when( $.get('./tmpl/person.tmpl'), $.getJSON('path/to/data') )
   .then(function( template, data ){
      $.tmpl( template, data ).appendTo( "#people" );
   });

The deferred.done() and then() methods will take as arguments the results from each function called in the when() function – in order – i.e. the output of the get will map to template, and the output from the getJson will be mapped to data.  This is pretty sweet!

Perhaps a simpler to observe example of the behavior is shown here: http://jsfiddle.net/austegard/ZaFVg/ - no prize for correct guesses as to the result of this…

/* Hello and World are both treated as resolved deferreds - they 
can be replaced with any function, like a $.get, etc */
$.when( "Hello", "World" ).then(
   function(x, y){ alert(x + " " + y); }
);

Labels: , , , ,

Tuesday, January 11, 2011

"Microsoft has completely lost the web development community."

Last year Mark Pilgrim released a free e-book/site called “Dive Into Html5” (http://diveintohtml5.org/).  The site/book has served as a valuable resource on a recent Html5 project we’re working on here at AIS, and I have frequently gone back for details on topics such as local storage and canvas.  It is an excellent book for any bleeding edge web developer.  It is so choice. If you have the means, I highly recommend picking one up.

This week, Mark posted his observations on how publishing a free e-book (which is also purchasable in print format) works well for him, and that it gives great insight into what parts of the book are being read, and by whom. He then makes the following observation:

6% of visitors used some version of Internet Explorer. That is not a typo. The site works fine in Internet Explorer — the site practices what it preaches, and the live examples use a variety of fallbacks for legacy browsers — so this is entirely due to the subject matter. Microsoft has completely lost the web development community. (emphasis mine)

I forwarded this internally within AIS, and a nice debate ensued.  One common complaint was the hyperbole of the statement, and I agree; a more accurate line would likely be "Microsoft as a browser vendor has lost significant mindshare in the bleeding edge web development community."

Personally one of the things I love about Html5 (using the term the way the hypers would – to mean modern web development with client-driven UI interactions using JavaScript, CSS(3) and some HTML5 semantics) is that it has in some ways unified the web development community:  The debate a few years ago was about JSP vs .NET vs PHP vs Python vs Rails vs someotherservertechnology.  Folks from different camps seldom interacted and learned from each other.  With Html5, the backend processes are completely irrelevant, as long as they don’t muck with the Html (ASP.NET webforms is still a major sinner here, unfortunately) and developers using all sorts of backend software and operating systems are now adding to the collective knowledge, mostly working towards the common goal of getting as much functionality as possible, pushed to end users through mostly standards compliant browsers. 

For instance, our Html5 app is backed by ASP.NET MVC 2 and SQL server.  We do all our development on Windows, in Visual Studio – we’re looking to deploy to Azure.  Clearly we’re MS developers.  But we could just as well have done the app in Php against MySql running on linux and apache, and we’re taking cues from folks using python, java, Rails, Node.js, php and God knows what on the backend.

At the same time I haven’t used IE by choice for about 5 years, maybe more…

I was asked what I thought MS could do to gain back some developer mindshare – so here goes:

  • My thoughts are that if Html5 and the set of bleeding edge technologists that go with it are any kind of priority for MS,  they need to do some or all of the following:
  • Find a way to upgrade the legions of IE 6, 7  and 8 users to IE9.  This will obviously not be easy,  but they could do something similar to what Google did with Chrome frame (i.e. make IE9 a plugin for the older browsers),  or they could do something like the makers of the “IE Tab” Chrome and Firefox extensions do,  allow IE to be hosted inside Chrome,  and only activate it for certain sites.  Or let users install IE9 side by side with the older versions.   All of these would have as goal to encourage end users to use the latest possible browser for the task they need it for,  and to make them install IE9 instead of Chrome or Firefox.
  • Make IE9 the paragon of standards compliance.   (They are actually getting close to this...)
  • Bring IE9 to WP7 and whatever tablet software they're coming out with.
  • Reduce the focus of Silverlight as a browser plugin,  and make it more about web-deployed desktop apps.
  • Drastically improve the support for css and javascript in Visual Studio, including debugging and unit testing.   And give this toolset away in the form of VS Express.
  • Evolve the Dev tools in IE9 to become better than Chrome's inspector and the Firebug plugin.
  • Separate the IE development from Windows to allow quicker iterations
  • Do more things like the jQuery deal. The world of CSS is a mess (we desperately need mixins and code forks like those provided by media queries), MS could take the lead here…

The point is, whether Mark’s browser percentages are statistically valid as an indication of web developer’s preferences, or to what degree Microsoft is lagging/losing developer mindshare; these are not the pertinent questions.  The fact is that Microsoft is now not a leader in emerging web development areas – maybe they never were – but should they want to be, they need to take action. IE9 is shaping up to be a great browser, and they need to push it aggressively.

Labels: , , , , , , , , , , , , , , , , , , ,

Wednesday, October 27, 2010

MS Word tip of the Day

The No-Width Optional Break symbol.  Heretofore known as NWOB™. Assign a shortcut key to it and use it, your word text flow (and readers) will thank you.

A NWOB™ is a symbol that tells Word that “don’t insert a visible space here, but if you need to break this text across two lines, this is a good place to do it.”  This comes in handy if you have to write Shakespearesque prose like the following:
“service provided as part of a combined authentication/‌authorization/‌payment module”. 

See how the lines wrap in an awkward, unsightly manner? (UPDATE: depending on your browser and the weather pattern, automatic breaks may actually already be inserted around the /s. Not so in Word - trust me.)

With the NWOB™, you too can flow your text gently to the margins of your document, leaving no unsightly large space behind in your document.  For just three easy payments of $19.99… To insert a NWOB™ go to the Insert tab and click Symbol, then More Symbols.  Then click the Special Characters tab, scroll down and locate the No-Width Optional Break.  Select it, and before you do anything hasty, add a shortcut key, now that you know about it, you’ll want to use the NWOB™ again.  (I use Ctrl+​​Shift+​​Alt+​​Space.)

So now your same Pulitzer worthy prose can flow more naturally:
“service provided as part of a combined authentication/​authorization/‌payment module”. 

PS! The &#8203; numeric reference is HTML4’s version of the NWOB™.  I use it above in my Ctrl+​Shift+​Alt+​Space shortcut. Note that annoyingly, Live Writer is not kind to this character, it removes it when you enter the Source panel.

Labels: ,

Friday, September 24, 2010

CSS3 Fun

Today I learned about two new CSS3 rules that both work in newish webkit (Chrome/Safari) browsers.  We can FINALLY vertically align elements in a div, without resorting to use/simulating of tables, by using box-sizing.  About freaking time.  And you can now also resize elements in the client.  I clearly need to read up more on what’s coming in CSS3…

Example (you'll need a newish webkit (Chrome/Safari) browser):
You can resize the div below – see how the image always stays centered?

Source:

<div style="display: -webkit-box; -webkit-box-align: center; -webkit-box-pack: center; resize: both; overflow: hidden; width: 200px; height: 200px; background-color: black;">
<img src="http://www.appliedis.com/images/tagline_01.gif" style="max-width:100%; max-height: 100%">

Labels: , , , ,

Tuesday, August 10, 2010

XElement, XAttribute and Explicit Conversion Operators

When we (my team at AIS) some xml parsing for the Rolling Stone project, we had to convert element and attribute values to ints and dates, etc. We ran into the problem that sometimes these elements and attributes didn’t exist – so using myXElement.Attribute(myAttribute).Value in a TryParse() would fail with a NullReferenceException, since Value couldn’t be called on a non-existent attribute.

Classic case for an Extension method – which is what I created:

public static T GetAttributeValueOrDefault(this XElement element, XName attributeName, T defaultValue) where T : struct
{
	//First check that the attribute is present
	XAttribute attribute = element.Attribute(attributeName);
	if (attribute == null || attribute.Value == null || attribute.Value == "null")
	{
		return defaultValue;
	}
	//...else attempt conversion
	return attribute.Value.ConvertOrDefault(defaultValue);
}

…except that we were all WRONG: http://www.hanselman.com/blog/ImprovingLINQCodeSmellWithExplicitAndImplicitConversionOperators.aspx

It turns out XAttribute has a series of explicit conversion operators, as does XElement:

Reflector reveals the following for XAttribute (XElement also has the same operators)

public class XAttribute : XObject
{
…

    // Methods
…
    [CLSCompliant(false)]
    public static explicit operator DateTime?(XAttribute attribute);
    [CLSCompliant(false)]
    public static explicit operator bool(XAttribute attribute);
    [CLSCompliant(false)]
    public static explicit operator Guid(XAttribute attribute);
    [CLSCompliant(false)]
    public static explicit operator bool?(XAttribute attribute);
    [CLSCompliant(false)]
    public static explicit operator int(XAttribute attribute);
    [CLSCompliant(false)]
    public static explicit operator Guid?(XAttribute attribute);
    [CLSCompliant(false)]
    public static explicit operator int?(XAttribute attribute);
…
    //etc, etc, etc – umpteen more
…

The implementation of the int? version looks like this;

[CLSCompliant(false)]
public static explicit operator int?(XAttribute attribute)
{
    if (attribute == null)
    {
        return null;
    }
    return new int?(XmlConvert.ToInt32(attribute.value));
}

So to get the value of an XElement/XAttribute, simply cast it to the type you want – make it the nullable variety if you aren’t sure that the attribute/element is present, the explicit conversion operator will check and convert the value for you, then check for null in your code…

Live and learn.

PS!  How are you expected to discover this, without reading the source-code (or Scott’s blog)?

 

  

Labels: , , ,

Friday, June 11, 2010

How to Always Run Visual Studio As Administrator

To jum straight to the solution, click here

“Certain tasks, including debugging and creating local IIS applications, require that you start Visual Studio as a user with Administrative privileges. On Windows Vista, and Windows Server 2008 when not running as the built-in Administrator account, this requires right-clicking the Visual Studio 2008 icon in the Start Menu and choosing Run as administrator.

“To make this process easier, you can create a shortcut and check the Run this program as an administrator check box on the Compatibility tab of the shortcut properties.”
from Using Visual Studio 2008 with IIS 7 @ learn.iis.net

On the last few projects I’ve worked on, we’ve used IIS sites for our development (for a number of reasons I won’t detail here), and the need to open VS in admin mode has been a constant annoyance.  It’s like constantly getting bitten by a mosquito. Today I finally got annoyed enough to spend 5 minutes researching a solution.  (I know.  I procrastinate.)

The solution, or what seems to be working for me so far at least was found at sevenforums.com: How to Run a Program as an Administrator in Windows 7.  Some of these options I knew about, the one I hadn’t tried and which worked for me was this:

1. Right click on the program shortcut or program .exe file, then click on Properties, and on the Compatibility tab. (See screenshots below)
NOTE: If you are doing this while logged on as a standard user instead of an administrator, then you will need to also click on the Change settings for all users button and type in the administrator's password.

Run as Administrator-compatibility_mode1.jpgRun as Administrator-compatibility_mode2.jpg

2. To Always Run this Program as an Administrator -

A) Check the Run this program as an administrator box, and click on OK. (See screenshots above)

The key is to change the compatibility setting of the Visual Studio EXECUTABLE, not the shortcut to it.  I.e., on my laptop, I went to C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ and right-clicked devenv.exe and then proceeded as above.

I then had to add one more step – when I now clicked on a .sln file, nothing would happen.  It appears the default Open action couldn’t run, I assume, due to inadequate privileges.  To fix this, I right-clicked the .sln file, selected Open With –> Choose Default Program, and then selected Visual Studio, making sure Always use… was checked.

Presto – my .sln files now open asking to be run as admin, as do my jump list projects.

Itch scratched.

Labels: , , , , , , , ,

Thursday, May 27, 2010

Enabling Support for HTML 5 Schema Validation in Visual Studio 2010

Out of the box, HTML 5 schema validation is not supported in Visual Studio 2008 and 2010.  The folks on the Visual Web Developer Team rectified this for VS 2008 and Visual Web Developer, but they did not provide any update for VS 2010 (that I know of).  No matter, you can use the 2008 version – here’s how.

1. Download the schema update for VS 2008/VWD

2. Open the downloaded zip file and follow the instructions in the ReadMe.txt file except:

2.a) Where it says to add the schema to C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\Packages\schemas\html, replace the 9.0 with 10.0 – the path for VS 2010 (in 64 bit Windows) is:

C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Packages\schemas\html

2.b) Edit the VS registry file matching your Windows environment, and again change the 9.0 to 10.0 – e.g. for my Windows 7 64 bit machine the correct registry file is:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\10.0\Packages\{1B437D20-F8FE-11D2-A6AE-00104BCC7269}\Schemas\Schema 23]
"File"="html\\html_5.xsd"
"Friendly Name"="HTML 5"
"URI"="http://schemas.microsoft.com/intellisense/html-5"

3. Back up your registry

4. Run the registry file (as always, take caution)

Labels: , , ,

Thursday, April 08, 2010

Uri Properties

For the life of me I can never remember what each property of the Uri class is meant to return.  SnippetCompiler to the rescue:

public static void RunSnippet()
{
  Uri foo = new Uri("http://some.domain.com/folder/file.htm?param=val#frag");
    foreach(PropertyInfo pi in foo.GetType().GetProperties()){
      if (pi.CanRead) {
        WL("{0}: {1}", pi.Name, pi.GetValue(foo, null));
      }
    }
}

Make sure using System.Reflection; and using System.Web; are is included, and there you have it:

AbsolutePath: /folder/file.htm
AbsoluteUri: http://some.domain.com/folder/file.htm?param=val#frag
Authority: some.domain.com
Host: some.domain.com
HostNameType: Dns
IsDefaultPort: True
IsFile: False
IsLoopback: False
IsUnc: False
LocalPath: /folder/file.htm
PathAndQuery: /folder/file.htm?param=val
Port: 80
Query: ?param=val
Fragment: #frag
Scheme: http
OriginalString: http://some.domain.com/folder/file.htm?param=val#frag
DnsSafeHost: some.domain.com
IsAbsoluteUri: True
Segments: System.String[]
UserEscaped: False
UserInfo:

UPDATE:  Steve Michellotti just showed me how this works beautifully in LinqPad as well, just set the language to C# Statement(s), replace WL with Console.WriteLine and you’re good to go.

Labels: , , , ,

Wednesday, January 06, 2010

MVC Route Constraint to Exclude Values

For Album Credits I wanted to allow personalized urls of the format http://albumcredits.com/yournamehere.  This turned out to be quite an interesting routing exercise.

Since this is an MVC app, our standard url format is of the usual http://albumcredits.com/{controller}/{action}/{index} kind, and for some pages, I need to allow the url to simply specify the controller, defaulting the action to index – again, the usual ASP.NET MVC pattern.

I was familiar with the constraint parameter option for the AddRoute method, but had never studied it in much detail – we’d used it to limit certain indexes to be numeric, but that was all.  For the root-level personalized urls we needed a more robust constraint – specifically we needed to exclude any controller from the list of valid personalized Urls.

I first spent more time than I cared to trying to come up with a regular expression pattern that would NOT match the list of controller names – it looked something like this:
^(?:(?!\b(foo|bar)\b).)*$
(thanks to Justin Poliey/stackoverflow.com) where foo and bar, etc were the controller names to NOT match.

Not until after I got that to work did I think to google “mvc custom route constraint”.  Of course the MS MVC team was smarter than that – custom route constraints are really very straight forward…

For my purposes, I went with David Hayden’s approach – the code below is essentially the same as his, just with the logic reversed.

using System;
using System.Linq;
using System.Web;
using System.Web.Routing;

namespace AlbumCredits.Web
{
	/// <summary>
	/// Route constraint that returns true if the parameter value is not one of the excluded values.
	/// </summary>
	/// <example>A controller constraint like 
	/// <code>new { controller = new ExcludeValuesConstraint("foo", "bar") }</code>
	/// will match "blah" or "snort" but will not match "foo" or "bar".
	/// </example>
	public class ExcludeValuesConstraint : IRouteConstraint
	{
		private readonly string[] _excludeValues;
		/// <summary>
		/// Initializes a new instance of the <see cref="ExcludeValuesConstraint"/> class.
		/// Example: <code>new { controller = new ExcludeValuesConstraint("foo", "bar") }</code>
		/// will match "blah" or "snort" but will not match "foo" or "bar".
		/// </summary>
		/// <param name="excludeValues">The excluded values.</param>
		public ExcludeValuesConstraint(params string[] excludeValues)
		{
			_excludeValues = excludeValues;
		}

		/// <summary>
		/// Determines whether the URL parameter contains a valid value for this constraint.
		/// </summary>
		/// <param name="httpContext">An object that encapsulates information about the HTTP request.</param>
		/// <param name="route">The object that this constraint belongs to.</param>
		/// <param name="parameterName">The name of the parameter that is being checked.</param>
		/// <param name="values">An object that contains the parameters for the URL.</param>
		/// <param name="routeDirection">An object that indicates whether the constraint check is being performed when an incoming request is being handled or when a URL is being generated.</param>
		/// <returns>
		/// true if the URL parameter contains a valid value; otherwise, false.
		/// </returns>
		public bool Match(HttpContextBase httpContext, Route route, string parameterName, 
			RouteValueDictionary values, RouteDirection routeDirection)
		{
			return !(_excludeValues.Contains(values[parameterName].ToString(), StringComparer.InvariantCultureIgnoreCase));
		}
	}
}

I can now use this when setting up my Route Table like this:

	routes.MapRoute("PersonalizedUrl",
		/* for urls like  */ "{personalizedUrl}",
		/* route defaults */ new { controller = MVC.Profile.Name, action = MVC.Profile.Actions.IndexByPersonalizedUrl, personalizedUrl = string.Empty },
		/* where          */ new { personalizedUrl = new ExcludeValuesConstraint(ControllerNameArray) }
	);

Labels: , , ,

Friday, December 04, 2009

Getting the ContentType from an ItemAdding Event Handler

Inside an event handler for an ItemAdded event, getting the List Item’s ContentType is as easy as

var contentType = properties.ListItem.ContentType;

However, for ItemAdding, properties.ListItem is null.

Luckily, the ContentType name and Id are part of the properties.AfterProperties collection – the following will work:

SPList list = properties.List; 
string contentTypeName = properties.AfterProperties["ContentType"].ToString(); 
SPContentType contentType = list.ContentTypes[contentTypeName]; 
SPFieldCollection fields = contentType.Fields;

Between the AfterProperties field values and the content type field collection, you typically have all that you need... Just remember to not depend on properties.ListItem.

Labels: , , , , ,

Tuesday, November 17, 2009

How to Upgrade a WSPBuilder Project to SharePoint 2010

<UPDATE date=”2009.12.02”>: WSPBuilder Extensions 2010 Beta 1.3 (or newer) makes the below mostly obsolete – though I was still not able to deploy using the VS add-in – I got a message stating my SPTimer Service was not running.</update>

Based on advice from Bjørn Furuknap (@furuknap, http://furuknap.blogspot.com/2009/11/using-wspbuilder-for-sharepoint-2010.html) I was able to deploy to SP 2010 a rather complex WSPBuilder-based SharePoint solution that I first recompiled in VS 2010.

Steps were:

  1. Download the command line version, with the x64 cablib dll
  2. Upgrade your VS 2008/2005 solution to VS 2010
  3. Replace your 12 references with their 14 equivalents - most will be found in C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI
  4. Compile your VS solution - this SHOULD work
  5. Use the wspbuilder command line tool to create, but not deploy your wsp (use wspbuilder -help for instructions)
  6. Use stsadm -o addsolution -filename <yourwsp> to deploy the solution

Labels: , , , , ,

Monday, November 09, 2009

SharePoint Office Server 2010, on Windows 7, in Google Chrome

image

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)…

image

… 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: , , , , , ,

Saturday, September 19, 2009

Mimicking Outlook 2010’s Conversation View in Outlook 2007

One of Outlook 2010 (and Gmail)’s better features is that conversations are grouped together – that is: if you send me a message, and I reply, and you reply again, all three messages are grouped together – not just the two you sent me.

The latter is what Outlook 2007 considers a “conversation” – I’d call it a monologue, or at best half a conversation.

To get the full duplex conversation thread in Outlook 2007 do the following:

  1. Right-click Search Folders, select new Search Folder
  2. In the New Search Folder dialog, scroll all the way to the bottom, and select Create a custom Search Folder, then click the Choose.. button.
  3. In the Custom Search Folder dialog, enter a name that makes sense to you – I’ll call it Inbox2.0 - SKIP THE CRITERIA, then click Browse… to select which folders to include.
  4. In the Select Folder(s) dialog, select the Inbox and Sent Items folders (and any subfolders/other folders that may make sense for your setup), then click OK
  5. Back in the Custom Search Folder dialog, click OK
  6. Outlook will present a warning that all items in the folders will be included – this is precisely what you want, so click Yes
  7. Your new (Inbox2.0) folder will appear listing all received and sent messages
  8. From the View menu, select Arrange By, Conversation – now your messages are threaded by conversation – the full duplex kind.
  9. From here, you may chose to remove/insert colums (I like to show the 'To' recipient and remove the 'In Folder', and also the 'Subject' column – it is (mostly) a duplicate of the conversation title.

Enjoy your new duplex view.

Labels: , , ,

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’
’xxxController._Views’
’T4MVC._Dummy’

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; }
}
#>
view raw T4MVC.tt hosted with ❤ by GitHub

Labels: , , , , , ,

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: , , , , , , ,

Making T4MVC comply with StyleCop

On a current MVC project we’re also using the excellent T4MVC template by David Ebbo.  StyleCop however, thinks the generated code is well, less than perfect – it generates some 500 warnings at the moment. 

The solution to this is a simple choice between two options:

Fix the TT file to generate StyleCop compliant code, or exclude the generated T4MVC.cs class from StyleCop.

The pragmatic choice here is of course to exclude the file.  But how?

I first tried to add <ExcludeFromStyleCop>true</ExcludeFromStyleCop> to the Compile entry in the csproj file. Unfortunately that only works with builds from OUTSIDE Visual Studio.

Sergey Shishkin has the answers:

Encapsulating the code in a region that contains the string “generated code” does the trick, but even easier is to simply put a // <auto-generated /> comment at the top of the generated file – which of course means edit the TT file to stick it there.

Would be nice to see this included in the next release….

Labels: , , , , ,