SitePoint Sponsor

User Tag List

Page 1 of 2 12 LastLast
Results 1 to 25 of 40
  1. #1
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)

    Arrow Rolling your own MVC app in ASP.NET (Tutorial)

    For those that like to delve, want customization of underlying abstract classes, or just don't like the way any existing framework behaves, this tutorial is for you.

    Personally, I don't like the way the ASP.NET MVC framework lumps action handlers into a single controller. I prefer to isolate my actions as much as possible.

    You will not need to install ASP.NET MVC framework, or any other framework, other than that which comes with Visual Studio 2008.

    To begin, use VS to create a new web application on your development server, or in the file system if you prefer.

    The first thing you will want to do is edit your web.config file and edit it the following changes.

    Add the following to assemblies:

    Code XML:
    <!-- required for routing -->
    <add assembly="System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
    <add assembly="System.Web.Abstractions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
    <!-- / required for routing -->

    Add the following to modules:

    Code XML:
    <!-- required for routing -->
    <remove name="UrlRoutingModule"/>
    <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
    <!-- / required for routing -->

    Add the following to handlers:

    Code XML:
    <!-- required for routing -->
    <add name="UrlRoutingHandler" preCondition="integratedMode" verb="*" path="UrlRouting.axd" type="System.Web.HttpForbiddenHandler, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
    <!-- / required for routing -->

    Change the authentication type to Forms:

    Code XML:
    <authentication mode="Forms" />

    Add the following attribute to modules:

    Code XML:
    <!-- set runAllManagedModulesForAllRequests="true" to persist FormsAuthentication after routing -->
    <modules runAllManagedModulesForAllRequests="true">

    The second thing you will want to do is set up the aspnetdb database. Use the ASP.NET Configuration tool to do this. Enabled roles and add one. Then add a user, adding him to the role. Make note of the username and password. We will use this later.

    Now let's get on to the code!

    Add a global.asax file to your project and use the following code:

    Code VBNET:
    <&#37;@ Application Language="VB"%>
    <%@ Import namespace="System.web.Routing" %>
    <script runat="server">
    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
    RouteTable.Routes.Add("ResourceStopRoute", New Route("{resource}.axd/{*pathInfo}", New StopRoutingHandler))
    Dim routeConstraints As RouteValueDictionary = New RouteValueDictionary
    routeConstraints.Add("id", "\d+")
     
    Dim routeDefaults As RouteValueDictionary = New RouteValueDictionary
    routeDefaults.Add("command", "Home")
    routeDefaults.Add("action", "Page")
    routeDefaults.Add("id", 0)
     
    Dim routeDefinition As Route = New Route("{command}/{action}/{id}", routeDefaults, routeConstraints, New RequestController)
    RouteTable.Routes.Add("CommandActionRoute", routeDefinition)
    End Sub
     
    </script>

    This sets a decimal value as the constraint for the id parameter and defaults for all, then adds the route. In addition, we add a route to ignore requests for axd resource files. Why do we not add constraints for the command and action parameters? You'll see shortly.

    Now add a code file name RequestController.vb to your code base and use this code:

    Code VBNET:
    Imports Microsoft.VisualBasic
    Imports System.Web
    Imports System.Web.Routing
    Imports System.Reflection.Assembly
    Public Class RequestController : Implements Routing.IRouteHandler
    Public Function GetHttpHandler(ByVal requestContext As RequestContext) As IHttpHandler Implements IRouteHandler.GetHttpHandler
    Dim commandName As String = New String("{command}{action}Command")
    commandName = commandName.Replace("{command}", requestContext.RouteData.Values("command"))
    commandName = commandName.Replace("{action}", requestContext.RouteData.Values("action"))
    Dim desiredCommand As IRouteHandler = GetExecutingAssembly.CreateInstance(commandName)
    If desiredCommand Is Nothing Then desiredCommand = New HomePageCommand
    Return desiredCommand.GetHttpHandler(requestContext)
    End Function
    End Class

    Here is where we create our command object based off of the command and action parameters. If it cannot be created, we use a known command instead. In this case, the home page. This is why we didn't add constraints earlier. We want all typos in user typed urls to actually go somewhere.

    Now here are the two abstract classes in our sysem. The CommandAbstract and ViewAbstract. You can add as much as you want to these and persist just about anything you wish between command and view.

    Code VBNET:
    Imports Microsoft.VisualBasic
    Imports System.Web
    Imports System.Web.Routing
    Public MustInherit Class CommandAbstract : Implements Routing.IRouteHandler
    Protected RouteData As Dictionary(Of String, Object) = New Dictionary(Of String, Object)
    Protected ViewData As Dictionary(Of String, Object) = New Dictionary(Of String, Object)
    Public Function GetHttpHandler(ByVal requestContext As RequestContext) As IHttpHandler Implements IRouteHandler.GetHttpHandler
    For Each item In requestContext.RouteData.Values
    RouteData.Add(item.Key, item.Value)
    Next
    Dim desiredView As IHttpHandler = Execute(requestContext)
    requestContext.HttpContext.Items.Add("RouteData", RouteData)
    requestContext.HttpContext.Items.Add("ViewData", ViewData)
    Return desiredView
    End Function
    Protected MustOverride Function Execute(ByRef requestContext As RequestContext) As IHttpHandler
    End Class
    Public MustInherit Class ViewAbstract : Implements IHttpHandler
    Protected RouteData As Dictionary(Of String, Object)
    Protected ViewData As Dictionary(Of String, Object)
    Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
    Get
    Return True
    End Get
    End Property
    Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
    RouteData = context.Items("RouteData")
    ViewData = context.Items("ViewData")
    Render(context)
    End Sub
    Protected MustOverride Sub Render(ByRef context As HttpContext)
    End Class

    All commands and views derive from these. Now let's take a look at the commands and views.Be sure to change the username and password in UserLoginCommand using the values you supplied earlier!

    Code VBNET:
    Imports Microsoft.VisualBasic
    Imports System.Web
    Imports System.Web.Routing
    Public Class AboutPageCommand : Inherits CommandAbstract
    Protected Overrides Function Execute(ByRef requestContext As RequestContext) As IHttpHandler
    ViewData("message") = "This is the about page!"
    Return New AboutPageView
    End Function
    End Class
    Public Class HomePageCommand : Inherits CommandAbstract
    Protected Overrides Function Execute(ByRef requestContext As RequestContext) As IHttpHandler
    Return New HomePageView
    End Function
    End Class
    Public Class UserLoginCommand : Inherits CommandAbstract
    Protected Overrides Function Execute(ByRef requestContext As RequestContext) As IHttpHandler
    If requestContext.HttpContext.Request.RequestType = "POST" Then
    If Membership.ValidateUser("Admin", "admin--") Then
    FormsAuthentication.SetAuthCookie("Admin", True)
    requestContext.HttpContext.Response.Redirect("~/")
    End If
    End If
    Return New UserLoginView
    End Function
    End Class
    Public Class UserLogoutCommand : Inherits CommandAbstract
    Protected Overrides Function Execute(ByRef requestContext As RequestContext) As IHttpHandler
    If requestContext.HttpContext.Request.RequestType = "POST" Then
    FormsAuthentication.SignOut()
    requestContext.HttpContext.Response.Redirect("~/")
    End If
    Return New UserLogoutView
    End Function
    End Class

    Code VBNET:
    Imports Microsoft.VisualBasic
    Imports System.Web
    Imports System.Web.Routing
    Public Class AboutPageView : Inherits ViewAbstract
    Protected Overrides Sub Render(ByRef context As System.Web.HttpContext)
    context.Response.Write(ViewData("message"))
     
    End Sub
    End Class
    Public Class HomePageView : Inherits ViewAbstract
    Protected Overrides Sub Render(ByRef context As System.Web.HttpContext)
    If context.User.Identity.IsAuthenticated Then
    context.Response.Write("You are currently logged in as: " + context.User.Identity.Name + "!")
    Else
    context.Response.Write("You are currently logged in as: Guest!")
    End If
    End Sub
    End Class
    Public Class UserLoginView : Inherits ViewAbstract
    Protected Overrides Sub Render(ByRef context As System.Web.HttpContext)
    context.Response.Write("<html><head><title></title></head><body><form method=""post"" action=""Login""><input type=""submit"" id=""submit"" name=""submit"" value=""Login Now"" /></form></body></html>")
    End Sub
    End Class
    Public Class UserLogoutView : Inherits ViewAbstract
    Protected Overrides Sub Render(ByRef context As System.Web.HttpContext)
    context.Response.Write("<html><head><title></title></head><body><form method=""post"" action=""Logout""><input type=""submit"" id=""submit"" name=""submit"" value=""Logout Now"" /></form></body></html>")
    End Sub
    End Class

    You should now be ready to compile and run.If you try this sample and have problems, let me know.

    Ideally, you will want to store your html templates externally and read them in, whether they be in static files, xml, or database. This leave a very clean system, in that there are NO aspx files whatsoever in the application (yes, this means you can delete the Default.aspx now). I store my localized phrases and templates in a db as bits and pieces are pull them in when needed, but that's beyond the scope of this little tutorial.

    I hope this was useful, or at least, insightful, to somebody.

  2. #2
    ALT.NET - because we need it silver trophybronze trophy dhtmlgod's Avatar
    Join Date
    Jul 2001
    Location
    Scotland
    Posts
    4,836
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I'm struggling to understand WHY you would do this?

    Personally, I don't like the way the ASP.NET MVC framework lumps action handlers into a single controller. I prefer to isolate my actions as much as possible.
    This isn't stricly true. You can have as many controllers as you want with as many actions as you want. If you really wanted to seperate your controller actions out as seperate classes, there are better ways to do this onto of the ASP.NET Mvc framework.

    Ideally, you will want to store your html templates externally and read them in, whether they be in static files, xml, or database. This leave a very clean system, in that there are NO aspx files whatsoever in the application (yes, this means you can delete the Default.aspx now). I store my localized phrases and templates in a db as bits and pieces are pull them in when needed, but that's beyond the scope of this little tutorial.
    What advantage do you see in not having any aspx files? Looking at your example, I see nothing but magic strings and headache!

    If you really don't like ASP.NET Mvc or the built in extensibility points, then you might want to check out FubuMvc http://code.google.com/p/fubumvc/

  3. #3
    SitePoint Author silver trophybronze trophy
    wwb_99's Avatar
    Join Date
    May 2003
    Location
    Washington, DC
    Posts
    10,638
    Mentioned
    4 Post(s)
    Tagged
    0 Thread(s)
    Ideally, you will want to store your html templates externally and read them in, whether they be in static files, xml, or database. This leave a very clean system, in that there are NO aspx files whatsoever in the application (yes, this means you can delete the Default.aspx now). I store my localized phrases and templates in a db as bits and pieces are pull them in when needed, but that's beyond the scope of this little tutorial.
    I can see scenarios where this might be useful. In any case, you don't need to kill off the entirety of MVC to handle this. Just write a view engine.

  4. #4
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Well, first and foremost, this article was written to simply demonstrate how you can create an MVC framework on your own, for maximum customizability, not to dictate exactly what one should do with it, or how. You could just as easy create the view abstract based off of the Page class and return a Precompiled instance of an aspx page which inherits from that class, using inline code to write stuff out. Again, the point was to demonstrate how to use the routing system, routehandler and httphandler classes only.

    As for the questions you all posed...

    I put several things in the db for various reasons. I'll cover them
    below.

    Locales table: holds things like dir, lang, charset, and other additional info about a locale, such as display name, whether or not it's a master (unalterable/undeletable) and so on. You can't do this with a resx.

    Phrases table: what you might see in a typical resx file, linked to a specific locale. If localeID = 0, it's the master phrase.

    Skins table: same kind of thing as with locales table. holds several settings that help define the skin, including whether or now it's selectable by the user.

    Templates table: same kind of thing as with phrases table, linked to a particular skin, if skinID = 0, it's a master template.

    Some examples of templates would be:

    <ul>
    {data:items}
    </ul>

    ...and...

    <li>{data:value}</li>

    Simple example and not real exciting, but I would be assembling the li's first, using stringbuilder then drop them into the ul, and so on.

    The main reason I do this is because early on I reaslized I needed more control over the html output than what is given to me via aspx / skinfile. There were a great many instances where, due to the severity of change between skins, the only way to make it work was to use things like <&#37;# Eval(???) %> or <%$ resource:??? %> in the skinfile, which you can't do. Therefore, I abandoned the use of aspx pages altogether.

    Second. I wanted to provide an online user interface that allowed one to alter the locale, phrase, skin and template definitions, upload additional ones, or author a new one, from within the application.

    Things in my apps are setup, in a lot of ways, like the admincp of this very forum.

  5. #5
    ALT.NET - because we need it silver trophybronze trophy dhtmlgod's Avatar
    Join Date
    Jul 2001
    Location
    Scotland
    Posts
    4,836
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Ok, fair point on the demo of how to use the route handler, but....

    It really looks like your trying to mold ASP.NET into a PHP application, and your not taking advantage of what the framework offers.

    I see what your doing for the localisation, but I don't understand why you can't use the the standard WebForms view engine, or even Spark. Why not use action filters to deliver the localisation with the use of a One Model In/Out convention that allows you to customise whats delivered, allowing for easy access to the database in a decoupled fashion and still taking advantage of the Mvc framework.

    We use the Spark view engine for parsing templates that are stored in a database for email templates and small regions of customisation on the sire. If you wanted to store the whole view in the database you can, as Wyatt pointed out, simply create a viewengine. Their super simple, and a database backed on would be very easy and you can still use all the features of WebForms.

    I have given presentations of most of the above and know how and easily these things can be done. While its far from perfect, the ASP.NET Mvc framework is highly extensible, and there really is no need to reinvent the wheel. We have completely changed the core parts of Mvc to deliver a more strongly typed generic development experience, simply replacing the parts we need. I would highly recommend checking out the source for the framework, and the source for Oxite.

  6. #6
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Why can't I use the webforms framework? I could if i chose to. That's what it comes down to. Choice. Factor in one more consideration: closed source. I do not want to use any third party things and leave as little open to 'tinkering' as possible. This is just the way I choose to do it. Everybody has their own ideas of what is best and can cite all the reasons in the world to support it, and I can appreciate that. My ideas simply differ from the majority. That's not wrong either. It's just different.

  7. #7
    ALT.NET - because we need it silver trophybronze trophy dhtmlgod's Avatar
    Join Date
    Jul 2001
    Location
    Scotland
    Posts
    4,836
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I never said you where wrong

    However, your closed source statement is a little confusing and mistaken. The entire framework is closed, however ASP.NET Mvc is not. You can download the source code and do whatever you want.

    I would also not refer to ASP.NET as a "third party things", it's a first class component of the .NET framework and is developed exclusivly by Microsoft.

    Our own ideas differ from the majority of ASP.NET Mvc users too, but using the extensibilty points supplied with ASP.NET Mvc we have changed it to our own opinions, making for a, in our eyes, better development experience, but still building upon a solid framework with commercial support. The way you develop is up to you, I'm simply trying to engage you in a discussion and help you understand the other options available to you as a .NET developer before you go off and reinvent the wheel. Also, discussion can lead to approaching things differently, and I am trying to understand what advantages you believe you will gain, t'is all

  8. #8
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Sometimes, the only way for people like me to understand the wheel is to reinvent it.

    Anyway, I have some questions regarding the ViewEngine article linked above. I was unaware that the mvc framework had this extension in it. I always assumed the paths were based on controller and action name, are were locked into the "one and only" view.

    So now I have a few questions about it, as the code was in c#, not vb, and incomplete:

    1. Are the arrays mentioned in the engine constructor public or protected members of the engine class?

    2) Are the methods shown in the next listing also members of the engine class?

    3) How does this new engine know exactly what to replace the placeholders with? I.E. I get the desired path from an option in the user profile. Assume it's "PurplePassion". How does the engine know to replace {0} with "PurplePassion"?

    I am assuming that's what this does right?

    Change ~/Views/{controller}/{action}.aspx
    To ~/Views/{skin}/{controller}/{action}.aspx

    I don't understand from the article, how to tell the system what value to use for {skin}.

  9. #9
    ALT.NET - because we need it silver trophybronze trophy dhtmlgod's Avatar
    Join Date
    Jul 2001
    Location
    Scotland
    Posts
    4,836
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I used to do the same, but I prefer to read source code now. You can find the ASP.NET Mvc source code here: http://www.microsoft.com/downloads/d...displaylang=en

    1) The highest level of abstraction involved here is the IViewEngine interface. The frameworks comes with two implementations, an abstract factory called VirtualPathProviderViewEngine and a concrete implementation called WebFormViewEngine. The arrays are public properties of the VirtualPathProviderViewEngine.

    2) The two methods in the next snippet are the abstract factory methods that VirtualPathProviderViewEngine requires to be implemented, and are not on the IViewEngine. The way this is done is a common technqiue used when developing frameworks on .NET (read the .NET Framework Design Guidelines for more info).

    3) Resolving the skin name would be up to you. I would suggest inheriting from the WebFormsViewEngine and overloading each of the IViewEngine methods. You can then set the path to the view yourself then delegate to the base method. The ControllerContext parameter of each method will give you access to all the info about the current request you should should need to make the decision of what skin needs to be applied:

    I quickly knocked up the below code, it's completely untested and in C#. If you use VB.NET, you can change the code here: http://www.developerfusion.com/tools.../csharp-to-vb/

    Code Csharp:
    public class QuickAndDirtySkinningEngine : WebFormViewEngine
        {
            private readonly VirtualPathProvider virtualPathProvider;
     
            public QuickAndDirtySkinningEngine(VirtualPathProvider virtualPathProvider)
            {
                this.virtualPathProvider = virtualPathProvider;
     
                MasterLocationFormats = new[]
                                            {
                                                "~/Shared/{0}.master"
                                            };
     
                ViewLocationFormats = new[]
                                          {
                                              "~/Views/{3}/{1}/{0}.aspx",
                                              "~/Views/{3}/{1}/{0}.ascx",
                                          };
     
                PartialViewLocationFormats = new[]
                                                 {
                                                     "~/Views/{3}/{1}/{0}.ascx",
                                                     "~/Shared/{0}.ascx"
                                                 };
            }
     
            public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
            {
                string skinName = controllerContext.RouteData.GetRequiredString("skin");
                string controllerName = controllerContext.RouteData.GetRequiredString("controller");
     
                IList<string> locationsSearched;
     
                string viewPath = FindFile(ViewLocationFormats, path => String.Format(path, controllerName, viewName, skinName), out locationsSearched);
                string masterPath = FindFile(MasterLocationFormats, path => String.Format(path, controllerName, masterName, skinName), out locationsSearched);
     
                if (!string.IsNullOrEmpty(viewPath) && !string.IsNullOrEmpty(masterPath))
                    return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
     
                return new ViewEngineResult(locationsSearched);
            }
     
            public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
            {
                string skinName = controllerContext.RouteData.GetRequiredString("skin");
                string controllerName = controllerContext.RouteData.GetRequiredString("controller");
     
                IList<string> locationsSearched;
     
                string partialViewPath = FindFile(PartialViewLocationFormats, path => String.Format(path, controllerName, partialViewName, skinName), out locationsSearched);
     
                if ( !string.IsNullOrEmpty(partialViewPath))
                    return new ViewEngineResult(CreatePartialView(controllerContext, partialViewPath), this);
     
                return new ViewEngineResult(locationsSearched);
            }
     
            private string FindFile(IEnumerable<string> locationsToSearch, Func<string, string> formatPath, out IList<string> locationsSearched)
            {
                string partialViewPath = string.Empty;
     
                locationsSearched = new List<string>();
     
                foreach (string pathTemplate in locationsToSearch)
                {
                    partialViewPath = formatPath(pathTemplate);
                    locationsSearched.Add(partialViewPath);
     
                    if (virtualPathProvider.FileExists(partialViewPath))
                        break;
                }
     
                return partialViewPath;
            }
        }

  10. #10
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    So if I modified that, and used it, all I'd have to do is do viewEngines.clear and then add this one? Sounds nice. I might give it a try when I fully wake up.

  11. #11
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)

    ASp.NET MVC ThemeEngine Question

    Based on code posted by another member, I came up with the following code.

    Code VBNET:
    Imports Microsoft.VisualBasic
    Imports System.Web
    Imports System.Web.Mvc
     
    Public Class ThemeEngine : Inherits WebFormViewEngine
     
    Public Sub New()
     
    MasterLocationFormats = New String() { _
    "~/Views/{1}/Masters/{0}.Master", _
    "~/Views/{1}/Common/{0}.Master", _
    "~/Views/Default/Masters/{0}.Master" _
    }
     
    ViewLocationFormats = New String() { _
    "~/Views/{2}/{1}/{0}.aspx", _
    "~/Views/{2}/Common/{0}.aspx", _
    "~/Views/Default/{1}/{0}.aspx" _
    }
     
    PartialViewLocationFormats = New String() { _
    "~/Views/{1}/Controls/{0}.ascx", _
    "~/Views/{1}/Common/{0}.ascx", _
    "~/Views/Default/Controls/{0}.ascx" _
    }
     
    End Sub
     
    Public Overloads Overrides Function FindView(ByVal controllerContext As ControllerContext, ByVal viewName As String, ByVal masterName As String, ByVal useCache As Boolean) As ViewEngineResult
     
    Dim themeName As String = controllerContext.HttpContext.Items("theme")
    Dim controllerName As String = controllerContext.RouteData.GetRequiredString("controller")
     
    Dim viewPath As String = String.Empty
    Dim masterPath As String = String.Empty
     
    Dim searchedLocations As List(Of String) = New List(Of String)()
     
    For Each viewLocationFormat As String In ViewLocationFormats
    viewPath = String.Format(viewLocationFormat, viewName, controllerName, themeName)
    searchedLocations.Add(viewPath)
    If VirtualPathProvider.FileExists(viewPath) Then Exit For
    Next
     
    For Each masterLocationFormat As String In MasterLocationFormats
    masterPath = String.Format(masterLocationFormat, masterName, themeName)
    searchedLocations.Add(masterPath)
    If VirtualPathProvider.FileExists(masterPath) Then Exit For
    Next
     
    If Not String.IsNullOrEmpty(viewPath) And Not String.IsNullOrEmpty(masterPath) Then
    Return New ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), Me)
    Else
    Return New ViewEngineResult(searchedLocations)
    End If
     
    End Function
     
    Public Overloads Overrides Function FindPartialView(ByVal controllerContext As ControllerContext, ByVal partialViewName As String, ByVal useCache As Boolean) As ViewEngineResult
     
    Dim themeName As String = controllerContext.HttpContext.Items("theme")
    Dim controllerName As String = controllerContext.RouteData.GetRequiredString("controller")
     
    Dim partialViewPath As String = String.Empty
    Dim searchedLocations As List(Of String) = New List(Of String)()
     
    For Each partialViewLocationFormat As String In PartialViewLocationFormats
    partialViewPath = String.Format(partialViewLocationFormat, partialViewName, themeName)
    searchedLocations.Add(partialViewPath)
    If VirtualPathProvider.FileExists(partialViewPath) Then Exit For
    Next
     
    If Not String.IsNullOrEmpty(partialViewPath) Then
    Return New ViewEngineResult(CreatePartialView(controllerContext, partialViewPath), Me)
    Else
    Return New ViewEngineResult(searchedLocations)
    End If
     
    End Function
     
    End Class

    I then added the following to the global.asax.vb file that the mvc project template included.

    Code VBNET:
    Sub Application_Start()
    RegisterRoutes(RouteTable.Routes)
    ViewEngines.Engines.Clear()
    ViewEngines.Engines.Add(New ThemeEngine)
    End Sub

    I then set the following in HomeController.

    Code VBNET:
    Function Index() As ActionResult
    ViewData("Message") = "Welcome to ASP.NET MVC!"
    HttpContext.Items("theme") = "Red"
    Return View()
    End Function

    The "Red" theme does not exist, but the "Default" theme does. I made a new folder under ~/Views and rearranged everything under that.

    I get the error:

    The file '/Views/Default/Masters/.Master' does not exist.

    If I force the code to return based on searchedLocations, I get the following:

    The view 'Index' or its master could not be found. The following locations were searched:
    ~/Views/Red/Home/Index.aspx
    ~/Views/Red/Common/Index.aspx
    ~/Views/Default/Home/Index.aspx
    ~/Views/Red/Masters/.Master
    ~/Views/Red/Common/.Master
    ~/Views/Default/Masters/.Master

    So I know the thing works, because it's finding the view file. For whatever reason, the masterName coming into the handling function is empty or null. I updated all the referencing aspx files to make sure the MasterPageFile is set right and that it exists.

    Any ideas on what's going on?

  12. #12
    SitePoint Wizard
    Join Date
    Dec 2003
    Location
    USA
    Posts
    2,582
    Mentioned
    29 Post(s)
    Tagged
    0 Thread(s)
    I don't do too much ASP.NET, so I don't have a personal opinion on this. I just wanted to comment on how much I love these type of debates. So much information that you wouldn't find anywhere else surfaces from them.

  13. #13
    ALT.NET - because we need it silver trophybronze trophy dhtmlgod's Avatar
    Join Date
    Jul 2001
    Location
    Scotland
    Posts
    4,836
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    That's what happens when you post untested code Again, more untested code, my original code with the bug sorted and in vb.net

    Code Csharp:
    Public Class QuickAndDirtySkinningEngine
        Inherits WebFormViewEngine
        Private ReadOnly virtualPathProvider As VirtualPathProvider
     
        Public Sub New(ByVal virtualPathProvider As VirtualPathProvider)
            Me.virtualPathProvider = virtualPathProvider
     
            MasterLocationFormats = New () {"~/Shared/{0}.master"}
     
            ViewLocationFormats = New () {"~/Views/{3}/{1}/{0}.aspx", "~/Views/{3}/{1}/{0}.ascx"}
     
            PartialViewLocationFormats = New () {"~/Views/{3}/{1}/{0}.ascx", "~/Shared/{0}.ascx"}
        End Sub
     
    Public Overloads Overrides Function FindView(ByVal controllerContext As ControllerContext, ByVal viewName As String, ByVal masterName As String, ByVal useCache As Boolean) As ViewEngineResult
            Dim skinName As String = controllerContext.RouteData.GetRequiredString("skin")
            Dim controllerName As String = controllerContext.RouteData.GetRequiredString("controller")
     
            Dim locationsSearched As IList(Of String)
     
    Dim viewPath As String = FindFile(ViewLocationFormats, Function(path) [String].Format(path, controllerName, viewName, skinName), locationsSearched)
    Dim masterPath As String = FindFile(MasterLocationFormats, Function(path) [String].Format(path, controllerName, masterName, skinName), locationsSearched)
     
    [b]If String.IsNullOrEmpty(viewPath) OrElse Not String.IsNullOrEmpty(masterName) AndAlso String.IsNullOrEmpty(masterPath) Then
                Return New ViewEngineResult(locationsSearched)
            End If[/b]
     
            Return New ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), Me)
        End Function
     
    Public Overloads Overrides Function FindPartialView(ByVal controllerContext As ControllerContext, ByVal partialViewName As String, ByVal useCache As Boolean) As ViewEngineResult
            Dim skinName As String = controllerContext.RouteData.GetRequiredString("skin")
            Dim controllerName As String = controllerContext.RouteData.GetRequiredString("controller")
     
            Dim locationsSearched As IList(Of String)
     
    Dim partialViewPath As String = FindFile(PartialViewLocationFormats, Function(path) String.Format(path, controllerName, partialViewName, skinName), locationsSearched)
     
            If Not String.IsNullOrEmpty(partialViewPath) Then
                Return New ViewEngineResult(CreatePartialView(controllerContext, partialViewPath), Me)
            End If
     
            Return New ViewEngineResult(locationsSearched)
        End Function
     
    Private Function FindFile(ByVal locationsToSearch As IEnumerable(Of String), ByVal formatPath As Func(Of String, String), ByRef locationsSearched As IList(Of String)) As String
            Dim partialViewPath As String = String.Empty
     
            locationsSearched = New List(Of String)()
     
            For Each pathTemplate As String In locationsToSearch
                partialViewPath = formatPath(pathTemplate)
                locationsSearched.Add(partialViewPath)
     
                If virtualPathProvider.FileExists(partialViewPath) Then
                    Exit For
                End If
            Next
     
            Return partialViewPath
        End Function
    End Class
    And for completness, the C# version
    Code Csharp:
    public class QuickAndDirtySkinningEngine : WebFormViewEngine
        {
            private readonly VirtualPathProvider virtualPathProvider;
     
            public QuickAndDirtySkinningEngine(VirtualPathProvider virtualPathProvider)
            {
                this.virtualPathProvider = virtualPathProvider;
     
                MasterLocationFormats = new[]
                                            {
                                                "~/Shared/{0}.master"
                                            };
     
                ViewLocationFormats = new[]
                                          {
                                              "~/Views/{3}/{1}/{0}.aspx",
                                              "~/Views/{3}/{1}/{0}.ascx",
                                          };
     
                PartialViewLocationFormats = new[]
                                                 {
                                                     "~/Views/{3}/{1}/{0}.ascx",
                                                     "~/Shared/{0}.ascx"
                                                 };
            }
     
            public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
            {
                string skinName = controllerContext.RouteData.GetRequiredString("skin");
                string controllerName = controllerContext.RouteData.GetRequiredString("controller");
     
                IList<string> locationsSearched;
     
                string viewPath = FindFile(ViewLocationFormats, path => String.Format(path, controllerName, viewName, skinName), out locationsSearched);
                string masterPath = FindFile(MasterLocationFormats, path => String.Format(path, controllerName, masterName, skinName), out locationsSearched);
     
                if (string.IsNullOrEmpty(viewPath) || !string.IsNullOrEmpty(masterName) && string.IsNullOrEmpty(masterPath))
                    return new ViewEngineResult(locationsSearched);
     
                return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
            }
     
            public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
            {
                string skinName = controllerContext.RouteData.GetRequiredString("skin");
                string controllerName = controllerContext.RouteData.GetRequiredString("controller");
     
                IList<string> locationsSearched;
     
                string partialViewPath = FindFile(PartialViewLocationFormats, path => string.Format(path, controllerName, partialViewName, skinName), out locationsSearched);
     
                if (!string.IsNullOrEmpty(partialViewPath))
                    return new ViewEngineResult(CreatePartialView(controllerContext, partialViewPath), this);
     
                return new ViewEngineResult(locationsSearched);
            }
     
            private string FindFile(IEnumerable<string> locationsToSearch, Func<string, string> formatPath, out IList<string> locationsSearched)
            {
                string partialViewPath = string.Empty;
     
                locationsSearched = new List<string>();
     
                foreach (string pathTemplate in locationsToSearch)
                {
                    partialViewPath = formatPath(pathTemplate);
                    locationsSearched.Add(partialViewPath);
     
                    if (virtualPathProvider.FileExists(partialViewPath))
                        break;
                }
     
                return partialViewPath;
            }
        }

  14. #14
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Hey thanks for returning. Actually, I did some more reading over at asp.net (the website) and found a few things.

    1) WHen returning View() from an action, it returns a NULL view. The handler uses the {action} RouteData value to try and locate the view, in addition to the {controller} value. It has no way to know what the master page name is. You need to return View("Index", "Site") instead.

    2) Private ReadOnly virtualPathProvider As VirtualPathProvider
    ^ is not needed as the base class already has one, unless you declare it as 'Shadows'.

    So my code was working as intended, I just wasn't passing it a value.

    Thanks for your help. =)

    P.S. I'll be adding the additional string.isnullorempty(masterPath) to my own code later.

  15. #15
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)

    Question MVC Models vs LINQ classes

    Now, I don't mean to start another debate, but I have a question about the asp.net mvc model and modelbinder design versus the classes generated by the dbml canvas.

    It seems to me that having the database and dbml already set in stone, and in place, that having to create mvc models for action input to bind to is a little redundant, since I'd have to create a new dbml class instance and transfer the data from the model to the dbml class.

    Wouldn't it be easier to just use the dbml classes directly? I know that I'd have a few more lines using the Request.QueryString and Request.Form but what the heck, I don't really care.

    In addition, the View.Model is of Object type, so technically, I could just pass in a dbml class. Say for instance, the UserModel (I had intended on renaming all my dbml classes like this) and because of the relationships defined, I also get all the nested data from related tables.

    What would you consider the best way to handle this? Is this method reasonable?

  16. #16
    ALT.NET - because we need it silver trophybronze trophy dhtmlgod's Avatar
    Join Date
    Jul 2001
    Location
    Scotland
    Posts
    4,836
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Yeah, it was rushed example code, so I did overlook a few things.

    Glad to be of help, if you have any other Mvc questions, don't hestitate to post

  17. #17
    SitePoint Author silver trophybronze trophy
    wwb_99's Avatar
    Join Date
    May 2003
    Location
    Washington, DC
    Posts
    10,638
    Mentioned
    4 Post(s)
    Tagged
    0 Thread(s)
    @Serenarules--I moved your question about data models into this thread, since it is now featured and we might as well try and keep the current MVC discussion here. More eyeballs, etc.

    As for the question--here is something to ponder. It isn't a database, or a dbml, it is a data model represented as an object graph. Testing-wise, you don't want to have a dependency on getting the database working to test your routing logic, for example.

  18. #18
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Alright, I think I understand the concept a bit better then. Within the scope of unit testing, it's better to have an abstracted model, then later, once things work, bind that to the classes exposed by whatever data source you want, be it xml, sql, and so on. Is that a correct statement?

  19. #19
    ALT.NET - because we need it silver trophybronze trophy dhtmlgod's Avatar
    Join Date
    Jul 2001
    Location
    Scotland
    Posts
    4,836
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I call SoC

    Sending your domain object to your views has a number of serious issues, more so when your using something like Linq2Sql or Entity Framework.

    L2S and EF are more less a projection of your database structure, and sending down the entity to the view is more or less the same as your view having knowledge about your DB structure. Which is an extremely bad thing. Also, who about additional data? Say a list of cities? How do you transport that down to the view easy?

    We use something called One Model In, One Model Out. Basically, our views expected something like this:

    Code Csharp:
      IPresentationModel<ViewModel>

    We have our own ActionInvoker that uses an IoC container to resolves the required IPresentationModel and returns that to the page.

    What does this buy us? Loose coupling. We use a collection of action filters that append data to the IPresentationModel istance, say the Navigation, cities etc.

    Whats the viewmodel? Thats an abstraction of our business object, so we might have a UserCreateViewModel, UserChangePasswordViewModel, each view/entity gets its own ViewModel. This means we can change the underlying data structure without changing the view, SoC.

    Thats the model out part, but we also use the same model for model in. So an action might look like this:

    Code Csharp:
    public ActionResult Update(Guid id)
        {
              User user = userRepository.RetrieveForId(id);
            // do your mapping
            return View(new UserUpdateModel());
        }
     
        public ActionResult Update(UserUpdateModel updateModel)
        {
             //do stuff
            return View(new UserUpdateSuccessModel);
        }

    Now, you might be thinking, I'm going to end up with alot of manual and boring mapping code. Well, you want to check out AutoMapper http://www.codeplex.com/AutoMapper to remove this

    Next issue is that you will end up with loads of classes. This is true, and I think a good thing. I really perfer classes that are small and tighly focused, does one thing and does it well. I find this makes it easier to maintain, and with an IDE tool like Resharper, navigation is breeze

    There is also another plus for return a view model. We use the view model name as a convention for defning the view, so a UserUpdateSuccessModel will use UserUpdateSuccessModelView, convention over configuration. This also means less if statements in your views, which means less logic

  20. #20
    ALT.NET - because we need it silver trophybronze trophy dhtmlgod's Avatar
    Join Date
    Jul 2001
    Location
    Scotland
    Posts
    4,836
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Wyatt raise a very good point of why you don't want your datacontext directly accessable in your Controller (if you follow Stackoverflow, you will know this is what they do and it's baaad). It's a good idea to work with an abstraction, ideally an interface, and don't let persistence information bleed into your controller.

    This means your persistence mechanism can change without affecting anything that isn't directly associated with it, and if you use some form of viewmodel, then your views are also shielded against this kind change too.

  21. #21
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Good points all, but I have a response to the WebSite vs Project debate.

    I just tested the following so I know it to be true.

    If you start a project by clicking "New Website" and choosing your developement server (we'll assume localhost), then proceed to add a few sample classes to your App_Code folder and a few test aspx pages, you can then click "Build" to make sure it all works, then click "Publish" and publish it out to the file system. If you look at what was output, you'll see that it does in fact output assemblies, ready for upload to a master server. All the extra files aren't there, just as a "New Project" type would.

    Considering the above, and my personal preference for the way it displays work files, I'll be sticking with the "WebSite" type projects for now.

    There are also some Reflection based reasons. If you are trying to instantiate something from a class name held in a string, using GetExecutingAssembly(className) it will fail when in the context of a "New Project". I'm not sure exactly why that is though, except of course, that until compiled there is no assembly yet, but I would've thought MS would have worked around that before releasing the ide.

  22. #22
    ALT.NET - because we need it silver trophybronze trophy dhtmlgod's Avatar
    Join Date
    Jul 2001
    Location
    Scotland
    Posts
    4,836
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    One web application differs from a web site in that web application generally encourages a greater physical seperation between logical layers. This generally means a web application is easier to develop with TDD as the core application can be ran outsite web context, if done properly.

    A web application will also build in a similar way to a web site, F5 kicking up the dev server and publish publishing the files. However, IIRC, web site don't work well with build scripts and CI servers. In larger development projects this is a huge issue.

    This article lays out some of the other reasons I'm not a fan of web sites http://aspnetresources.com/blog/web_...oject_wap.aspx

    There are a number of ways to achieve strongly typed reflection, so I've never ran into your issue before.

    A website compiles on the fly IIRC, whereas an application needs compiled and this is probably what your issue is.

  23. #23
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Well I just read the article you link and maybe it's because I've never done a large scale app that I cannot see the difference.

    I always develop and build on the dev server, but compile and publish to file system before uploading to real server. It generates the same kind of output. An App_Data folder, a bin folder with dll's and config xml's in it, and two top level config xml's. Oh, and any aspx stub needed. The way I do it, there is nothing to compile once on the real server. It just runs. And no, I don't go gather up dependancies and drop them all the in the projects bin either...lol.

    In the end, for me, it's simply a matter of one works for me, the other doesn't. It doesn't mean I'm going to forego things like orginization and namespaces, or abstractions and proper inheritance.

    If I ever find out though why the Web Application Project type doesn't work for me, and can fix, I'll use it. Until then, I don't have much choice.

    =/

  24. #24
    ALT.NET - because we need it silver trophybronze trophy dhtmlgod's Avatar
    Join Date
    Jul 2001
    Location
    Scotland
    Posts
    4,836
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I'm extremely biased as I get a lot benefits from web application project due to the size of applications I work on and using TDD/CI

    Maybe you would like to start another thread on your reflection issue?

  25. #25
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    I just might do that. =) Only after I eat though. I'm hun-ga-reee...


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •