>
Blog
Book
Portfolio
Search

5/3/2012

11341 Views // 0 Comments // Not Rated

A Really Sweet MVC 3 Project Template: Part 2 – The Web Project

In Part 1 of this series, I introduced my version of a template for ASP.NET MVC 3 projects that contains the data access layer, a common library, and some other components that I tend to reuse on every site. Before diving into the final slice of the pie, the web project itself, I want to reiterate that this template isn't supposed to be the end-all and be-all of MVC projects; it's the stuff I need 90% of the time; use the cloner (code that'll be included in this post that clones folder structures, replaces guids, and other fun stuff) and strip out what you don't need.

Starting from the Visual Studio solution we created in Part 1, add a new MVC 3 Web Application. Choose the "Empty" template, which only provisions the main MVC folders (models, views, and controllers) and pulls in the latest jQuery, ASP.NET AJAX, and modernizer JavaScript files, and a few other goodies. Next add references to our ClientB.Common and ClientB.Data projects.

I usually end up deleting the "Content" folder and all the theme crap under it in favor adding a "Styles" folder with a single Site.css stylesheet under it. All the jQuery theming stuff is cute and all, but it just feels a bit overkill to me. Besides, these assets aren't really "content;" the content is in the MVC views. But I'll leave it in the template (however, the stylesheet will be moved into the above-menioned "Styles" folder) in case you need it. Included in my CSS are a few pervasive styles that I've found useful on many sites I've done. Here they are:

Code Listing 1

  1. html
  2. {
  3. overflow-y:scroll;
  4. }
  5. body
  6. {
  7. margin:0px;
  8. height:100%;
  9. font-size:10pt;
  10. font-family:arial;
  11. }
  12. textarea
  13. {
  14. padding:5px;
  15. resize:none;
  16. outline:none;
  17. height:100px;
  18. font-family:arial;
  19. }
  20. textarea:focus,
  21. input:focus
  22. {
  23. outline:none;
  24. }
  25. a
  26. {
  27. font-size:10pt;
  28. text-decoration:none;
  29. }
  30. a:focus
  31. {
  32. outline:none;
  33. }
  34. a img
  35. {
  36. border:none;
  37. }
  38. input[disabled="disabled"],
  39. input.disabled
  40. {
  41. cursor:default;
  42. color:graytext;
  43. }
  44. input[type="text"]
  45. {
  46. padding:5px;
  47. }
  48. input[type="submit"]
  49. {
  50. margin:0px;
  51. border:none;
  52. padding:0px;
  53. color:#0071bc;
  54. font-size:10pt;
  55. cursor:pointer;
  56. background:transparent;
  57. }
  58. .label
  59. {
  60. color:#000000;
  61. font-size:9pt;
  62. }
  63. .labelerror
  64. {
  65. border:none;
  66. color:#ff0000;
  67. font-size:9pt;
  68. }

To round out the folder maintenance, I also create an "Images" folder at the root to store visual assets, as well as add a catch-all JavaScript file named "ClientB.js" to the "Scripts" folder. The next big chunk to tackle is the web.config file. We're going to be adding not only app settings and connection strings, but also configuring the ASP.NET Membership provider, making things easier when working with areas, and a few other miscellaneous updates.

First, the connection strings. Copy in the one for Entity Framework in the Data project's app.config file with a name "Database" and pull out the actual connection string for another entry called "Membership." This second, more conventional one will be used by our authentication provider. Here's what it should look like:

Code Listing 2

  1. <connectionStrings>
  2. <add name="Membership" connectionString="[dev connection string]" />
  3. <add name="Database connectionString="metadata=res://*/Entities.csdl|res://*/Entities.ssdl|res://*/Entities.msl;provider=System.Data.SqlClient;provider connection string=&quot;[dev connection string]&quot;" providerName="System.Data.EntityClient" />
  4. </connectionStrings>

Next we want to use the Visual Studio 2010 feature that allows you to map web.config file modifications to build configurations. Ideally, your "Debug" settings in Configuration Manager match your local / development / staging / test servers, and "Release" mode matches production. If you have different environments or multiple developers, you might want to create additional configurations. This is way better than checking a web.config file into TFS with commented out settings blocks for each team member. Adorn each setting with the following transformation attributes where appropriate in the web.config.release file:

Code Listing 3

  1. <?xml version="1.0"?>
  2. <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  3. <appSettings>
  4. <add key="URL" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" value="http://clientb.com" />
  5. </appSettings>
  6. <connectionStrings>
  7. <clear />
  8. <add name="Membership" xdt:Transform="SetAttributes" xdt:Locator="Match(name)" connectionString="[prod connection string]" />
  9. <add name="Database" xdt:Transform="SetAttributes" xdt:Locator="Match(name)" connectionString="metadata=res://*/Entities.csdl|res://*/Entities.ssdl|res://*/Entities.msl;provider=System.Data.SqlClient;provider connection string=&quot;[prod connection string]&quot;" providerName="System.Data.EntityClient" />
  10. </connectionStrings>
  11. <system.web>
  12. <compilation xdt:Transform="RemoveAttributes(debug)" />
  13. </system.web>
  14. </configuration>

This way, when you build in release (or whatever) mode, Visual Studio will poop out the appropriately-generated web.config file. Notice that one of the app settings here is the URL to the site. Why, you might ask, would some of these settings be here while others are implemented as constants in ClientB.Utilities? Basically, if a string is dynamic (in terms of environment) it should be an app setting. A good example of this is an Email address from which automated notifications are sent. In development, you might want to use your own; in production, a generic one should be selected. However, something like the name of the site or an Id that's being used for a third party integration (like a credit card merchant account) will be constant across all configurations. I throw these in the "constants" class to avoid hard coding and allow site-wide changes (or clones for new sites) to be executed easily and safely.

Next is a tweak that makes areas able to consume assets in the "root" of the application. I'm not too versed in the details here, but it's one of those "do this and it works" kind of things. Take the following blob from the web.config file under "Views" and move it to the root one:

Code Listing 4

  1. <system.web.webPages.razor>
  2. <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  3. <pages pageBaseType="System.Web.Mvc.WebViewPage">
  4. <namespaces>
  5. <add namespace="System.Web.Mvc" />
  6. <add namespace="System.Web.Mvc.Ajax" />
  7. <add namespace="System.Web.Mvc.Html" />
  8. <add namespace="System.Web.Routing" />
  9. </namespaces>
  10. </pages>
  11. </system.web.webPages.razor>

Next, to make EF assemblies available application-wide, add the following under system.web/compilation/assemblies:

Code Listing 5

  1. <add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />

Now let's get into the authentication stuff. First of all, here's the basic goo that's needed to wire up the membership and role providers:

Code Listing 6

  1. <membership>
  2. <providers>
  3. <clear />
  4. <add name="AspNetSqlMembershipProvider" enablePasswordRetrieval="true" type="System.Web.Security.SqlMembershipProvider" connectionStringName="Membership" enablePasswordReset="true" requiresQuestionAndAnswer="true" requiresUniqueEmail="true" passwordFormat="Encrypted" maxInvalidPasswordAttempts="2147483647" minRequiredPasswordLength="7" minRequiredNonalphanumericCharacters="1" passwordAttemptWindow="1" applicationName="/" />
  5. </providers>
  6. </membership>
  7. <roleManager enabled="true">
  8. <providers>
  9. <clear />
  10. <add name="AspNetSqlRoleProvider" connectionStringName="Membership" applicationName="/" type="System.Web.Security.SqlRoleProvider" />
  11. </providers>
  12. </roleManager>

That's real basic stuff, but with a bunch of settings defaults I usually go with that are still worth mentioning. In the membership provider, I have it configured to have unique Email and encrypted passwords while allowing them to be retrieved, question and answer turned off, as well as account lockouts disabled. You can't really "disable" this, so they need to get int.MaxValue invalid attempts in one second to get locked out. Finally, I have password strengths that require six characters with at least one being non-alphanumeric.

Finally, in order to facilitate password encryption and decryption, we need to have a valid machine key in the web.config file. These should really be unique for each web app you write, so I use the following generator to get a new one. Here's an example of what an entry (in system.web along with the rest of the authentication configuration) looks like, with the encrypted bits stripped out:

Code Listing 7

  1. <machineKey validationKey="[encoded]" decryptionKey="[also encoded]" validation="SHA1" decryption="AES" />

Next we have the Email section. I like to wire this up in the web.config file so that the Email code I have in ClientB.Common will pick up those settings and just work. Much like with the connection strings, these settings might differ from development to production, so use your web.config.whatever files to manage them. Here's what that block looks like:

Code Listing 8

  1. <system.net>
  2. <mailSettings>
  3. <smtp deliveryMethod="Network">
  4. <network host="[SMTP server URL]" userName="[Username or Email address]" password="[password]" />
  5. </smtp>
  6. </mailSettings>
  7. </system.net>

That's it for configuration! Next up is to discuss is error handling. There are a lot of different ways to implement error handling, as well as a lot of third party tools that can take care of it for you. However, using an external component sort of goes against the grain of a generic template. I'll throw some shameless pugs out for Elmah or Log4net as they are the two I feel are used the most, and then go ahead and do my own.

There are basically three parts to error handling in my eyes: throwing errors, catching errors, and reporting errors. You won't find any examples of the former in this template, because it's more ethereal in the first place and I never encounter errors at all to any extent in the second place. Juuust kidding. Real quick though, throwing errors means deciding which exceptions are handled and which are unhandled. When you're building a particular functionality, ask yourself: "What if this bombs?" "What if there are no elements in this collection?" "What if it's null?"

Basically, if it could be null in some supported (or at least identified) use cases, I protect it with a try...catch. Or if the code is dealing with user input, and I can't anticipate every possible situation: try it and catch it. But if something should not be null, I purposely do not protect it; this exception has to be thrown so I know about it. Catching and eating is way better for your bug count and way worse for your application than throwing into the wind.

On the backend, in ClientB.Common, have some sort of static class through which all errors are routed. When you do purposefully catch an exception, call this method explicitly. What it actually does is up to your implementation. You can throw it in an event log, log file, log database table, or just Email it to yourself if that's the preferred route. It doesn't matter, as long as the "catching" part of the architecture handles them uniformly. But when an unhandled exception occurs, and you don't think the generic ASP.NET error page works well aesthetically in your application, we need to wire up higher-level logging.

And nothing is higher-level than Global.asax. Here of course is where you can handle global events in your application around start up, session, and, as it turns out, Application_Error! This event will be raised when any unhandled exception occurs in the app. Here's my implementation:

Code Listing 9

  1. protected void Application_Error(object sender, EventArgs e)
  2. {
  3. try
  4. {
  5. //initialization
  6. Exception exception = this.Server.GetLastError();
  7. if (exception != null)
  8. {
  9. //clear
  10. this.Response.Clear();
  11. this.Server.ClearError();
  12. //email
  13. Utilities.SendErrorEmail(exception.ToString(), "Global Error Handler");
  14. //return
  15. this.Response.Redirect(string.Concat("~/Home/Error/?message=", Convert.ToBase64String(Encoding.ASCII.GetBytes(exception.Message))));
  16. }
  17. }
  18. catch (Exception ex)
  19. {
  20. //we're really broken
  21. Utilities.SendErrorEmail(ex.ToString(), "Global Error Handler Error");
  22. this.Response.Redirect("~/Home/Error");
  23. }
  24. }

The only interesting thing here is in Line #15 when I base 64-encode the error message and pass that explicitly to a view via querystring. Although I haven't tried it (because it's hard to experiment with error handling logic when you have never had an error in your entire career) it might be a bit cleaner to use session or some other mechanism. But when you're handling global errors, you have no idea what might be broken; I always try to respect this and write the leanest code possible here.

So that's throwing and catching; what about reporting? The Emailing is included in the template because Email is always available for error logging; using a database or a log file or a third party control requires some "intrusion" into the architecture and/or surface area of the application. Email will more likely than not already "be" there, so for this template, we'll just piggy back that tier.

When we're still in the context of the website when an unhandled exception occurs, we need to add a UI to the error reporting in addition to the logging. This way, users don't have a false positive when something didn't work. Like I said, it's far worse to hide these errors than it is to show them; your users will be a little annoyed because an error occurred in general, but appreciate the fact that you spent a few extra minutes to make the experience as graceful as possible.

The view display is up to you; let's take a look at the controller that renders it:

Code Listing 10

  1. public ActionResult Error(string message)
  2. {
  3. //get message
  4. if (string.IsNullOrWhiteSpace(message))
  5. message = "An unknown error has occured.";
  6. else
  7. {
  8. try
  9. {
  10. //get message
  11. message = Encoding.ASCII.GetString(Convert.FromBase64String(message));
  12. }
  13. catch
  14. {
  15. //busted
  16. message = "Unable to decode error message.";
  17. }
  18. }
  19. //render view
  20. this.ViewBag.Message = message;
  21. return this.View();
  22. }

Basically, it takes in the encoded error message, and attempts to decode it before passing everything down to the view for rendering. The only reason I screw around with encoding is because I don't like passing human-readable text in query strings. Not only is it sloppy, and lets users see up the skirt of your implementation, but I feel it could be a real easy to get hacked.

That's it for error logging! I won't go into too much detail for the error page itself; just make sure to slap a link on there so they can get back to the home page. You'll notice that the Visual Studio MVC 3 template includes an "Error" view in the "Shared" folder, and that view has code in it that unhinges itself from its layout. This, like my statement about code in the global error handler, is a protection against not knowing what's broken; it could be something in the layout. So be careful not to cause errors by handling errors!

<Internet Noise>

I'm not putting this into the template, but I wanted to call it out real quick. With some of my little MVC 3 sites that are in production, (incidentally all hosted on GoDaddy) I get a lot of HttpExceptions regarding forgery tokens and other "Internet noise." I'm not sure if it cause by dying spam robots defeated by my MVC security measures, actual errors in my URL mappings, or something else entirely. I've found myself adding logic to my global error handling that swallows these, as they are sort of impossible to reproduce and fix. I hate eating exceptions almost as much as I hate Outlook taking ten minutes to load. Just something to think about...

</ Internet Noise>

Let's move on to the controllers, which are of course the brains behind the MVC pattern. Right click the "Controllers" folder and select "Add..." and then "Controller..." In the box that pops up, type in "Base" over the selected text so that the class name becomes "BaseController" and click "Add." We are going to have all of our other controller inherit from this one, so sort of think of it like the code-behind for a master page.

The two pieces of logic I'm including in this template are there to demonstrate two different paradigms of what you'd want to do with a base controller. The first one, which is basic navigation logic, sets Booleans based on which controller is handling the request so the layout can update itself accordingly (by building out a breadcrumb or selecting tab or highlighting a background). Paradigm-wise, this is example of code that needs to run on each load; master pages (or the equivalent) are a great place to take care of these tasks.

Also using the ViewBag, the second sets the current user, which will ideally be cached to avoid database hits on each load. This is the second paradigm a base controller implements: handling do-once-and-cache tasks across the application. Let's look at the code:

Code Listing 11

  1. using System;
  2. using ClientB.Data;
  3. using System.Web.Mvc;
  4. namespace ClientB.Web.Controllers
  5. {
  6. public class BaseController : Controller
  7. {
  8. #region Events
  9. protected override void OnActionExecuting(ActionExecutingContext filterContext)
  10. {
  11. //initialization
  12. base.OnActionExecuting(filterContext);
  13. //get calling controller name
  14. string url = filterContext.Controller.ValueProvider.GetValue("controller").RawValue.ToString().ToLowerInvariant();
  15. //TODO: use url to update navigation
  16. //this.ViewBag.CurrentUrl = url;
  17. //if (url.Equals("home", StringComparison.InvariantCultureIgnoreCase))
  18. //{
  19. //this.ViewBag.Home = true;
  20. //this.ViewBag.Account = false;
  21. //}
  22. //else
  23. //{
  24. //this.ViewBag.Home = false;
  25. //this.ViewBag.Account = true;
  26. //}
  27. //TODO: caching of common data elements
  28. //open database
  29. using (Database database = new Database())
  30. {
  31. //set current user
  32. this.ViewBag.CurrentUser = database.GetCurrentUser();
  33. }
  34. }
  35. #endregion
  36. }
  37. }

Whichever paradigm you're following, use base controllers to do the things that happen upon every request. This differs from ActionFilterAttributes, which instead encapsulate logic that could happen on every instance of a particular request. Whereas every single request cares about the current user (base controller), only certain requests need to make sure the current user is authenticated (action filter attribute). The following are out-of-the-box filters that you'll see all over the place in the source code included with this post:

  1. HttpPost
  2. ValidateAntiForgeryToken
  3. Authorize

Like I said, these apply common functionality to particular requests. A cool thing about these filters is that they can be applied to both controllers and action methods. Applying it to a controller is the equivalent to applying it to all action methods on that controller. A good example of functionality like this is an attribute that redirects a request to its HTTPS equivalent. If you have a particular request or controller that handles credit card transactions, decorate it with this attribute. If your entire application requires SSL, put it on your base controller.

From here on in, everything else is optional. I'll be discussing some common patterns or functionality that I include in some, but not all, my applications. The ideas above (and in the previous post) are part of almost all of the MVC work I do. The rest are edge cases, but certainly still "common" enough to be represented in (and potentially ignored or removed from) the template. To start, here's an example of the above attribute, called ForceSSL:

Code Listing 12

  1. using System;
  2. using System.Web;
  3. using System.Web.Mvc;
  4. namespace ClientB.Web
  5. {
  6. public class ForceSSL : ActionFilterAttribute
  7. {
  8. #region Events
  9. public override void OnActionExecuting(ActionExecutingContext filterContext)
  10. {
  11. //initialization
  12. HttpRequestBase request = filterContext.HttpContext.Request;
  13. HttpResponseBase response = filterContext.HttpContext.Response;
  14. //check if we're secure or not and if we're on the local box
  15. if (!request.IsSecureConnection && !request.IsLocal)
  16. {
  17. //rebuild url to use ssl
  18. UriBuilder builder = new UriBuilder(request.Url)
  19. {
  20. Port = 443,
  21. Scheme = Uri.UriSchemeHttps
  22. };
  23. //redirect
  24. response.Redirect(builder.Uri.ToString());
  25. }
  26. //base
  27. base.OnActionExecuting(filterContext);
  28. }
  29. #endregion
  30. }
  31. }

Not only is this a good example of the flexibility of filter attributes specifically, but it also shows just how much control you have in MVC in general. Take advantage of how close you are to the underlying HTTP request. In web forms, these would have to be HTTP modules or handlers (which are sort of obscured from the rest of your code); in MVC, they are right there with the rest of your business logic.

The next optional controller I'm including is the handling of "robots" for SEO purposes. Instead of dealing with a robots.txt file, (which is very un-MVC) you can rig up a path to a controller that does this dynamically. Here's that controller:

Code Listing 13

  1. public ContentResult Robots()
  2. {
  3. //return
  4. return new ContentResult()
  5. {
  6. ContentType = "text/plain",
  7. Content = Convert.ToBoolean(ConfigurationManager.AppSettings["AllowRobots"]) ? "User-agent: *\r\nAllow: /" : "User-agent: *\r\nDisallow: /"
  8. };
  9. }

Few things here:

  • Line #1: This returns a ContentResult, which we can tell the browser that it's getting the plain text that the little Bing and Google robots re hungry for.
  • Line #7: I have a value in the web.config that controls this behavior. For the template, it's an "on" or "off" idea so that our code hanging out in production isn't being crawled while the site is still under development. However, you can add to this to enable certain crawlers, etc.
  • We need to enable this route in Global.asax. In the "RegisterRoute" method, before the last line representing the "default" route, add the following line:

Code Listing 14

  1. //robots
  2. routes.MapRoute("Robots", "Robots.txt", new { area = string.Empty, controller = "Home", action = "Robots" });

Speaking on ContentResults, there's also the nifty FileResult that your actions can return. I use this particularly to map a URL directly to my site's logo. This is useful in both the layout and in the Email template. First, store the relative path (something like "/images/logo.png") in the web.config. Then, create the following action (all of these "optional" actions are on the "Home" or default controller):

Code Listing 15

  1. public FilePathResult Logo()
  2. {
  3. //render image
  4. return this.File(this.Server.MapPath(ConfigurationManager.AppSettings["LogoImageURL"]), MediaTypeNames.Image.Jpeg);
  5. }

Line #4 has the "System.Net.Mime" namespace "using-ed" in. With this mechanism, "http://clientb.local/home/logo" will return the main image for the site. Finally, let's talk about the account stuff. After all of that wiring of the membership provider, we need to throw some UI into the app to facilitate all of the account-ish things users will need: registration, login/off, password request/change, etc. The "MVC 3 Internet" Visual Studio template pulls a lot of this stuff in for us, but I like to reorganize it a bit.

First, some sanity. There are UI services that come along with the out-of-the-box Account models, and the code is intermingled with them. I feel like service-typed things and POCO-typed things should not be neighbors. I like to refactor these services (and their interfaces) from Models/AccountModels.cs into separate classes on the root, named Services.cs and IServices.cs respectively.

There are even more refactoring opportunities in this file. The next thing I do is move all of the custom validators into a separate file. So create "Validation.cs" in the root of ClientB.Web and copy-and-paste anything that inherits from ValidationAttribute there. There's nothing specific about this code (or the services above) to the Account controller, so let's make it available to our entire site. I've also added some other validators that I use from time to time.

There's one more thing to do here, refactoring wise. There's a static method forlornly tucked into the "Status Codes" region that translates "proprietary" membership error codes into English strings. I move this guy, called "ErrorCodeToString," to the Utilities.cs class over in ClientB.Common. (Make sure you add a reference to the obscure System.Web.ApplicationServices assembly.) Once again, this logic not specific to the Account stuff, or even necessarily to our site, so its home should be Common.

Now we can get to the actual M in the MVC. What's left over are a few models describing the data that needs to be transferred to facilitate out-of-the-box authentication-ish functionality. Namely, these are the "ChangePasswordModel," "LogOnModel," and the "RegisterModel." I've basically kept these in fact in the template, but added "RetrievePasswordModel" and some other goodies. Here's what that looks like:

Code Listing 16

  1. public class RetrievePasswordModel
  2. {
  3. #region Members
  4. [Required]
  5. [ValidateEmail(true)]
  6. [DataType(DataType.EmailAddress)]
  7. [DisplayName("Email address")]
  8. public string Email { get; set; }
  9. #endregion
  10. }

Next we need to build out the controller and views needed to implement these models. Now I'm not going to audaciously re-write Microsoft templates as pass them off as my own; the Account controller and views are fine as they are. I only tore apart the models because that file, in my opinion, is a bit sloppy. So all I'll do is add in the infrastructure to support password retrieval, which follows the same patterns as the rest of the out-of-the-box functionality here.

First, let's pull our nicely-refactored services into Controllers/AccountController.cs:

Code Listing 17

  1. #region Members
  2. public IMembershipService _membershipService { get; set; }
  3. public IFormsAuthenticationService _formsService { get; set; }
  4. #endregion
  5. #region Protected Methods
  6. protected override void Initialize(RequestContext requestContext)
  7. {
  8. //set forms service
  9. if (this._formsService == null)
  10. this._formsService = new FormsAuthenticationService();
  11. //set membership service
  12. if (this._membershipService == null)
  13. this._membershipService = new AccountMembershipService();
  14. //initialization
  15. base.Initialize(requestContext);
  16. }
  17. #endregion

And then our password retrieval controller functionality:

Code Listing 18

  1. public ActionResult RetrievePassword()
  2. {
  3. //render view
  4. return this.View();
  5. }
  6. [HttpPost]
  7. [ValidateAntiForgeryToken]
  8. public ActionResult RetrievePassword(RetrievePasswordModel model)
  9. {
  10. //email user thier password
  11. if (this.ModelState.IsValid)
  12. {
  13. //get password
  14. if (this._membershipService.RetrievePassword(model.Email))
  15. {
  16. //success
  17. return this.RedirectToAction("RetrievePasswordSuccess");
  18. }
  19. else
  20. {
  21. //error
  22. this.ModelState.AddModelError(string.Empty, "Unable to retrieve password. Please try again.");
  23. }
  24. }
  25. //render view
  26. return this.View();
  27. }
  28. public ActionResult RetrievePasswordSuccess()
  29. {
  30. //render view
  31. return this.View();
  32. }

The views are fairly straight forward, so I won't make this epic post any longer than it was to be.

And that is it! Attached to this post are two ZIP files: one for the MVC project itself, and one for the project cloner. To use the later, unpackage it, and run it as a command line exe or load it up in Visual Studio. If you're in VS, edit the project file, and enter the following as the "Command line arguments" in the "Debug" tab:

-s "[full path to root folder to clone]" -t "[full path to root target folder]" -o -g -n "[taget namespace]" -i bin -i obj -i pkg -i pkgobj -e vspscc -e vssscc -sourcename "[source namespace]"

This code is actually just a utility used by an in-house TFS generation app, and hasn't really been used extensively outside of that context; manipulate it at your own risk! As for the MVC project, unzip it and set it up in Visual Studio as you would any other. You'll see that there's a lot more in there than I write about because, again, this post is way too long. Like I keep saying, this is just a starting point; strip out what you don't need, apply your own patterns and styles, and, as always, have fun MVC-ing!

No Tags

No Files

No Thoughts

Your Thoughts?

You need to login with Twitter to share a Thought on this post.


Loading...