Create custom view switcher as Mvc4 Mobile build-in mechanism - c#

Is it possible to create my own custom "view switcher" like MVC4 mobile does?
for example:
If I change my view name to "_Layout.Mobile.cshtml" it'll be automatically render the view if the user-agent is mobile device.
I want to manage couple of sites in one project so I want to allow by configuration(web.config) to call the view with the same way.
for example:
Account.Project1.cshtml (if configured to project 1)
Account.Project2.cshtml (if configured to project 2)
Account.Mobile.cshtml (if called from mobile)
Account.Project1.Mobile.cshtml (if called from mobile and project 1)
Account.cshtml (default)
I tried to google about it but i didn't found any question about this subject.
Thanks.

You can expand the DefaultDisplayMode:
public class MobileDisplay : DefaultDisplayMode
{
public MobileDisplay()
// postfix of the file
: base("mobile")
{
// create an expression if the current postfix is applicatble
ContextCondition = context => context.Request.Browser.IsMobileDevice;
}
}
And you can insert the in the app start:
DisplayModeProvider.Instance.Modes.Insert(0, new MobileDisplay());
This works like the routes and exceptions. First match win so that is why my example insert it to index 0.
The layout is not choosen by this. You can set the layout view per cshtml file or in the _ViewStart.cshtml file.
Of course You can set the mobile layout only in the concrete view or set in the _ViewStart base on logic:
#{
if (Request.Browser.IsMobileDevice)
{
Layout = "Shared/_Layout.mobile.cshtml";
}
else
{
Layout = "Shared/_Layout.cshtml";
}
}

Related

Is there a way in a Razor Pages app to change the default starting route to an Area?

When you create an ASP.Net Core Web App with Individual Accounts it adds an Identity Area along with the regular Pages folder.
Since I don't want to be working between the Pages folder along with the Area folder, I want to create a Home Area that my app's default route "/" will route to.
I was able to accomplish this for the Index page by adding this to Program.cs:
builder.Services.AddRazorPages(options =>
{
options.Conventions.AddAreaPageRoute("Home", "/Index", "");
});
Obviously, this just takes care of the single Index page, but if I wanted to have an About or Privacy page, I would have to add AreaPageRoutes for them as well. I tried a couple different things with AddAreaFolderRouteModelConvention that I am too embarrassed to show here, but was ultimately unable to find out how to make it work.
Is it possible and secondly, is it bad practice, to map the default routing of Pages to a specific Area?
You can use a PageRouteModelConvention to change the attribute route for pages in an area:
public class CustomPageRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
foreach (var selector in model.Selectors.ToList())
{
selector.AttributeRouteModel.Template = selector.AttributeRouteModel.Template.Replace("Home/", "");
}
}
}
Generally, my advice regarding areas in Razor Page applications is don't use them. They add additional complexity for no real gain at all. You have already found yourself fighting against the framework as soon as you introduced your own area. QED.
The only place areas serve a real purpose is within Razor class libraries, so you have to live with an area when using the Identity UI. But you don't have to use the Identity UI. You can take code inspiration from it and implement your own version in your existing Pages folder.

ASP.NET Core MVC View Component search path

In the documentation here:
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-2.2
The runtime searches for the view in the following paths:
/Views/{Controller Name}/Components/{View Component Name}/{View Name}
/Views/Shared/Components/{View Component Name}/{View Name}
/Pages/Shared/Components/{View Component Name}/{View Name}
How can I add another path here?
I'd like to have my view components with their respective controllers in one project folder named components like this.
/Components/{View Component Name}/{View Name}
My motivation:
I find out my view components have their own JS and CSS files. I have all JS bundled and minimized in one site.min.js and all CSS bundled and minimized in their site.min.css. The JS are always something like $(function() { ... }) and the CSS are always written in a way that order does not matter so bundling all without knowing the order is not a problem.
Some of those view components have javascripts which change their state on server e.g. AJAX call to a controller's action which returns some JSON or the whole view component's HTML.
Since Controllers are just a C# classes they can be in any folder but it feels stupid to move the controller with the relevant AJAX action to the "Views" folder.
In the end I'd like to have a "component" (not really a "view component" only) like this:
/Components/SomeViewComponent/Default.cshtml
/Components/SomeViewComponent/SomeViewComponentController.cs
/Components/SomeViewComponent/SomeViewComponent.cs
/Components/SomeViewComponent/SomeViewComponent.css
/Components/SomeViewComponent/SomeViewComponent.js
/Components/SomeViewComponent/SomeViewComponent.en.resx
/Components/SomeViewComponent/SomeViewComponent.cs-CZ.resx
So after an hour digging into the aspnetcore repository, I found the component's search path is hardcoded and then combined with normal view search paths.
// {0} is the component name, {1} is the view name.
private const string ViewPathFormat = "Components/{0}/{1}";
This path is then sent into the view engine
result = viewEngine.FindView(viewContext, qualifiedViewName, isMainPage: false);
The view engine then produces the full path, using the configurable view paths.
Views/Shared/Components/Cart/Default.cshtml
Views/Home/Components/Cart/Default.cshtml
Areas/Blog/Views/Shared/Components/Cart/Default.cshtml
If you want to place your view components into a root folder named "Components" as I wanted, you can do something like this.
services.Configure<RazorViewEngineOptions>(o =>
{
// {2} is area, {1} is controller,{0} is the action
// the component's path "Components/{ViewComponentName}/{ViewComponentViewName}" is in the action {0}
o.ViewLocationFormats.Add("/{0}" + RazorViewEngine.ViewExtension);
});
That's kind of ugly in my opinion. But it works.
You can also write your own expander like this.
namespace TestMvc
{
using Microsoft.AspNetCore.Mvc.Razor;
using System.Collections.Generic;
public class ComponentViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
// this also feels ugly
// I could not find another way to detect
// whether the view name is related to a component
// but it's somewhat better than adding the path globally
if (context.ViewName.StartsWith("Components"))
return new string[] { "/{0}" + RazorViewEngine.ViewExtension };
return viewLocations;
}
public void PopulateValues(ViewLocationExpanderContext context) {}
}
}
And in Startup.cs
services.Configure<RazorViewEngineOptions>(o =>
{
o.ViewLocationExpanders.Add(new ComponentViewLocationExpander());
});
You can add additional view location formats to RazorViewEngineOptions. As an example, to add a path that satisfies your requirement, you can use something like this:
services
.AddMvc()
.AddRazorOptions(o =>
{
// /Components/{View Component Name}/{View Name}.cshtml
o.ViewLocationFormats.Add("/{0}.cshtml");
o.PageViewLocationFormats.Add("/{0}.cshtml");
});
As can be seen above, there are different properties for views (when using controllers and actions) and page views (when using Razor Pages). There's also a property for areas, but I've left that out in this example to keep it marginally shorter.
The downside to this approach is that the view location formats do not apply only to view components. For example, when looking for the Index view inside of Home, Razor will now also look for Index.cshtml sitting at the root of the project. This might be fine because it's the last searched location and I expect you're not going to have any views sitting at the root of your project, but it's certainly worth being aware of.

How to view an ASP.NET MVC mobile page on Visual Studio 2012

I have an open source ASP.NET MVC(nopcommerce) or nopcommerce.com it is developed in ASP.NET MVC and razor view, it is both desktop and mobile version for example it has index.cshtml and Index.Mobile.cshtml but I have no experience on mobile viewing,
I do highly appreciate if someone give a a clue on how to view mobile pages on the browsers like Chrome and IE.
In MVC you would declare display-modes
For example in the link above they declare a mode called WP, to access that mode you would have a index.cshtml (normal mode) and a index.wp.cshtml (detected mode)
Once you got that, you can set up all the modes you like by testing pretty much anything you like
For example my /APP_Start/DisplayModeConfig.cs
public class DisplayModeConfig
{
public static void RegisterDisplayModes(DisplayModeProvider provider)
{
// INFO: Allows to name views/partials/masters like viewname.iphone.cshtml, and MVC will choose this automatically
// INFO: Lets remove the default "Mobile" mode, since it's pretty useless
var mobileDefault = DisplayModeProvider.Instance.Modes.First(m => m.DisplayModeId == "Mobile");
if (mobileDefault != null)
{
DisplayModeProvider.Instance.Modes.Remove(mobileDefault);
}
// INFO: Now add one that actually works
provider.Modes.Insert(0,
new DefaultDisplayMode("Mobile")
{
ContextCondition = (context => (!string.IsNullOrEmpty(context.GetOverriddenUserAgent()) && Regex.IsMatch(context.GetOverriddenUserAgent(), #"mobile|android|kindle|silk|midp", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)))
});
// INFO: Order from least to most important (since we insert at position 0)
provider.Modes.Insert(1,
new DefaultDisplayMode("Win8")
{
ContextCondition = (context => (!string.IsNullOrEmpty(context.GetOverriddenUserAgent()) && context.GetOverriddenUserAgent().IndexOf("Windows NT 6.2", StringComparison.OrdinalIgnoreCase) >= 0))
});
}
}
You also have a bunch of goodies in #Request.Browser.* like #Request.Browser.IsMobileDevice, #Request.Browser.Version, ..most work fine, but i would test those helpers before relying on them.
Depending on how mobile detection done by the site you may need:
tweak "user agent" string coming from browser. There are many tools/plugins for that.
set some sort of cookies
see if "use mobile view" supported directly on the site (i.e. http://www.xbox.com have such link at the bottom).

MVC Razor Hidden input and passing values

I am pretty sure I am doing something wrong here. I have been developing a web app using MVC and Razor and I never thought of using the form element. Now so much has already been done with master pages and sub pages that it means restructuring most of our code in order to use form element and the would result in multiple form elements on a page.
That aside, in Asp.Net if I wanted to access any control in the C# code behind I could just give it an ID="SomeID" and a RUNAT="SERVER". Then in my code behind I could set its value and properties.
When I do this in Razor, I use lines like:
<input id="hiddenPostBack" runat="server" type="hidden" />
Why can't I access this in the controller? I want to detect a postback and set the value to false if it is the first time the page loads, and if not, then set the value to true. Then based on this, I will read it either server side or client side and do something.
My real question is, how do I "do something" both server side and client side given that I don't have a form element. I was under the impression that if I wanted to pass values from client to server and back, the easiest way to do this is with a hidden input. But I am just not getting how to accomplish this with MVC3 and razor.
A move from WebForms to MVC requires a complete sea-change in logic and brain processes. You're no longer interacting with the 'form' both server-side and client-side (and in fact even with WebForms you weren't interacting client-side). You've probably just mixed up a bit of thinking there, in that with WebForms and RUNAT="SERVER" you were merely interacting with the building of the Web page.
MVC is somewhat similar in that you have server-side code in constructing the model (the data you need to build what your user will see), but once you have built the HTML you need to appreciate that the link between the server and the user no longer exists. They have a page of HTML, that's it.
So the HTML you are building is read-only. You pass the model through to the Razor page, which will build HTML appropriate to that model.
If you want to have a hidden element which sets true or false depending on whether this is the first view or not you need a bool in your model, and set it to True in the Action if it's in response to a follow up. This could be done by having different actions depending on whether the request is [HttpGet] or [HttpPost] (if that's appropriate for how you set up your form: a GET request for the first visit and a POST request if submitting a form).
Alternatively the model could be set to True when it's created (which will be the first time you visit the page), but after you check the value as being True or False (since a bool defaults to False when it's instantiated). Then using:
#Html.HiddenFor(x => x.HiddenPostBack)
in your form, which will put a hidden True. When the form is posted back to your server the model will now have that value set to True.
It's hard to give much more advice than that as your question isn't specific as to why you want to do this. It's perhaps vital that you read a good book on moving to MVC from WebForms, such as Steve Sanderson's Pro ASP.NET MVC.
If you are using Razor, you cannot access the field directly, but you can manage its value.
The idea is that the first Microsoft approach drive the developers away from Web Development and make it easy for Desktop programmers (for example) to make web applications.
Meanwhile, the web developers, did not understand this tricky strange way of ASP.NET.
Actually this hidden input is rendered on client-side, and the ASP has no access to it (it never had). However, in time you will see its a piratical way and you may rely on it, when you get use with it. The web development differs from the Desktop or Mobile.
The model is your logical unit, and the hidden field (and the whole view page) is just a representative view of the data. So you can dedicate your work on the application or domain logic and the view simply just serves it to the consumer - which means you need no detailed access and "brainstorming" functionality in the view.
The controller actually does work you need for manage the hidden or general setup. The model serves specific logical unit properties and functionality and the view just renders it to the end user, simply said. Read more about MVC.
Model
public class MyClassModel
{
public int Id { get; set; }
public string Name { get; set; }
public string MyPropertyForHidden { get; set; }
}
This is the controller aciton
public ActionResult MyPageView()
{
MyClassModel model = new MyClassModel(); // Single entity, strongly-typed
// IList model = new List<MyClassModel>(); // or List, strongly-typed
// ViewBag.MyHiddenInputValue = "Something to pass"; // ...or using ViewBag
return View(model);
}
The view is below
//This will make a Model property of the View to be of MyClassModel
#model MyNamespace.Models.MyClassModel // strongly-typed view
// #model IList<MyNamespace.Models.MyClassModel> // list, strongly-typed view
// ... Some Other Code ...
#using(Html.BeginForm()) // Creates <form>
{
// Renders hidden field for your model property (strongly-typed)
// The field rendered to server your model property (Address, Phone, etc.)
Html.HiddenFor(model => Model.MyPropertyForHidden);
// For list you may use foreach on Model
// foreach(var item in Model) or foreach(MyClassModel item in Model)
}
// ... Some Other Code ...
The view with ViewBag:
// ... Some Other Code ...
#using(Html.BeginForm()) // Creates <form>
{
Html.Hidden(
"HiddenName",
ViewBag.MyHiddenInputValue,
new { #class = "hiddencss", maxlength = 255 /*, etc... */ }
);
}
// ... Some Other Code ...
We are using Html Helper to render the Hidden field or we could write it by hand - <input name=".." id=".." value="ViewBag.MyHiddenInputValue"> also.
The ViewBag is some sort of data carrier to the view. It does not restrict you with model - you can place whatever you like.
As you may have already figured, Asp.Net MVC is a different paradigm than Asp.Net (webforms). Accessing form elements between the server and client take a different approach in Asp.Net MVC.
You can google more reading material on this on the web. For now, I would suggest using Ajax to get or post data to the server. You can still employ input type="hidden", but initialize it with a value from the ViewData or for Razor, ViewBag.
For example, your controller may look like this:
public ActionResult Index()
{
ViewBag.MyInitialValue = true;
return View();
}
In your view, you can have an input elemet that is initialized by the value in your ViewBag:
<input type="hidden" name="myHiddenInput" id="myHiddenInput" value="#ViewBag.MyInitialValue" />
Then you can pass data between the client and server via ajax. For example, using jQuery:
$.get('GetMyNewValue?oldValue=' + $('#myHiddenInput').val(), function (e) {
// blah
});
You can alternatively use $.ajax, $.getJSON, $.post depending on your requirement.
First of all ASP.NET MVC does not work the same way WebForms does. You don't have the whole runat="server" thing. MVC does not offer the abstraction layer that WebForms offered. Probabaly you should try to understand what controllers and actions are and then you should look at model binding. Any beginner level tutorial about MVC shows how you can pass data between the client and the server.
You are doing it wrong since you try to map WebForms in the MVC application.
There are no server side controlls in MVC. Only the View and the
Controller on the back-end. You send the data from server to the client by
means of initialization of the View with your model.
This is happening on the HTTP GET request to your resource.
[HttpGet]
public ActionResult Home()
{
var model = new HomeModel { Greeatings = "Hi" };
return View(model);
}
You send data from client to server by means of posting data to
server. To make that happen, you create a form inside your view and
[HttpPost] handler in your controller.
// View
#using (Html.BeginForm()) {
#Html.TextBoxFor(m => m.Name)
#Html.TextBoxFor(m => m.Password)
}
// Controller
[HttpPost]
public ActionResult Home(LoginModel model)
{
// do auth.. and stuff
return Redirect();
}

Search for .cshtml in multiple locations in MVC 3?

When a user comes to my site, there may be a template=foo passed in the query string. This value is being verified and stored in the Session.
My file layout looks like this:
- Views/
- Templates/
- test1/
- Home
- Index.cshtml
- test2/
- Home
- List.cshtml
- Home/
- Index.cshtml
Basically, if a user requests Index with template=test1, I want to use Views/Templates/test1/Index.cshtml. If they have template=test2, I want to use Views/Home/Index.cshtml (because /Views/Templates/test2/Home/Index.cshtml doesn't exist). And if they do not pass a template, then it should go directly to Views/Home.
I'm new to MVC and .NET in general, so I'm not sure where to start looking. I'm using MVC3 and Razor for the view engine.
You can do this by creating a custom RazorViewEngine and setting the ViewLocationFormats property. There's a sample here that does it by overriding the WebFormViewEngine, but using the RazorViewEngine should work just as well:
public class CustomViewEngine : WebFormViewEngine
{
public CustomViewEngine()
{
var viewLocations = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx",
"~/AnotherPath/Views/{0}.ascx"
// etc
};
this.PartialViewLocationFormats = viewLocations;
this.ViewLocationFormats = viewLocations;
}
}
You could modify Scott Hanselman's Mobile Device demo to fit your needs. Instead of checking the user agent or if it's a mobile device, you could put your logic in to check the query string or your Session vars.

Categories

Resources