I've had a MVC 4 / Entity web project dropped into my lap, and I've never used either before. I'm getting through it but the simple things are really tripping me up - Like hiding or displaying a link in my layout based on a parameter in the database.
I created a function in my HomeController that simply sets 2 bools in the ViewBag for whether or not a person is a manager or superuser. I call that function using
#Html.Action("SetupViewBag", "Home")
which sits right after the <body> tag in my layout. Here is the code for SetupViewBag:
public void SetupViewBag()
{
ViewBag.IsManager = ADAccess.IsManager(SessionManager.GetUserName());
ViewBag.IsSuper = SessionManager.SuperUser();
}
The bools are set properly and in the right order when I set up break points, but when I try to access them using the below code, I get a 'Cannot convert null to 'bool' because it is a non-nullable value type.'
#{
if((bool)#ViewBag.IsManager){
<li>#Html.ActionLink("Management", "Management", "Home",null, new { id = "managementLink" })</li>
}
}
There has to be something really simple I'm missing. Any help is greatly appreciated.
Based on your comment, #Dakine83, you should setup your ViewBag on the controller constructor method like so:
public class YourController : BaseController {
public YourController(){
}
}
The reason for that is because the Layout page is already rendered the time your action method has been called. The reason you have a null ViewBag.IsManager.
UPDATE: Use a base controller
public class BaseController : Controller {
public BaseController() {
ViewBag.IsManager = ADAccess.IsManager(SessionManager.GetUserName());
}
}
i hope this might work for you,please try it once
#Html.ActionLink("Management", "Management", "Home", new { id = false }, null);
Thanks
Related
I've created a GitHub repo to better understand the problem here. I have two actions on two different controllers bound to the same route.
http://localhost/sameControllerRoute/{identifier}/values
[Route("sameControllerRoute")]
public class FirstController : Controller
{
public FirstController()
{
// different EF Core DataContext than SecondController and possibly other dependencies than SecondController
}
[HttpGet("{identifier}/values")]
public IActionResult Values(string identifier, DateTime from, DateTime to) // other parameters than SecondController/Values
{
return this.Ok("Was in FirstController");
}
}
[Route("sameControllerRoute")]
public class SecondController : Controller
{
public SecondController()
{
// different EF Core DataContext than FirstController and possibly other dependencies than FirstController
}
[HttpGet("{identifier}/values")]
public IActionResult Values(string identifier, int number, string somethingElse) // other parameters than FirstController/Values
{
return this.Ok("Was in SecondController");
}
}
Since there are two matching routes, the default ActionSelector fails with:
'[...] AmbiguousActionException: Multiple actions matched. [...]'
which is comprehensible.
So I thought I can implement my own ActionSelector. In there I would implement the logic that resolves the issue of multiple routes via same logic depending on the 'identifier' route value (line 27 in code)
If 'identifier' value is a --> then FirstController
If 'identifier' value is b --> then SecondController
and so on...
protected override IReadOnlyList<ActionDescriptor> SelectBestActions(IReadOnlyList<ActionDescriptor> actions)
{
if (actions.HasLessThan(2)) return base.SelectBestActions(actions); // works like base implementation
foreach (var action in actions)
{
if (action.Parameters.Any(p => p.Name == "identifier"))
{
/*** get value of identifier from route (launchSettings this would result in 'someIdentifier') ***/
// call logic that decides whether value of identifier matches the controller
// if yes
return new List<ActionDescriptor>(new[] { action }).AsReadOnly();
// else
// keep going
}
}
return base.SelectBestActions(actions); // fail in all other cases with AmbiguousActionException
}
But I haven't found a good solution to get access to the route values in ActionSelector. Which is comprehensible as well because ModelBinding hasn't kicked in yet since MVC is still trying to figure out the Route.
A dirty solution could be to get hold of IHttpContextAccessor and regex somehow against the path.
But I'm still hoping you could provide a better idea to retrieve the route values even though ModelBinding hasn't happend yet in the request pipeline.
Not sure that you need to use ActionSelector at all for your scenario. Accordingly, to provided code, your controllers works with different types of resources (and so they expect different query parameters). As so, it is better to use different routing templates. Something like this for example:
FirstController: /sameControllerRoute/resourceA/{identifier}/values
SecondController: /sameControllerRoute/resourceB/{identifier}/values
In the scope of REST, when we are talking about /sameControllerRoute/{identifier}/values route template, we expect that different identifier means the same resource type, but different resource name. And so, as API consumers, we expect that all of the following requests are supported
/sameControllerRoute/a/values?from=20160101&to=20170202
/sameControllerRoute/b/values?from=20160101&to=20170202
/sameControllerRoute/a/values?number=1&somethingElse=someData
/sameControllerRoute/b/values?number=1&somethingElse=someData
That is not true in your case
I ended up implementing the proposed solution by the ASP.NET team. This was to implement an IActionConstrain as shown here:
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
namespace ActionConstraintSample.Web
{
public class CountrySpecificAttribute : Attribute, IActionConstraint
{
private readonly string _countryCode;
public CountrySpecificAttribute(string countryCode)
{
_countryCode = countryCode;
}
public int Order
{
get
{
return 0;
}
}
public bool Accept(ActionConstraintContext context)
{
return string.Equals(
context.RouteContext.RouteData.Values["country"].ToString(),
_countryCode,
StringComparison.OrdinalIgnoreCase);
}
}
}
https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.ActionConstraintSample.Web/CountrySpecificAttribute.cs
Laravel 5.1 has just been released, I would like to know how could I tell the AuthController to get the login & register view from a custom directory? the default is: resources/views/auth...
The trait AuthenticateAndRegisterUsers only has this:
trait AuthenticatesAndRegistersUsers
{
use AuthenticatesUsers, RegistersUsers {
AuthenticatesUsers::redirectPath insteadof RegistersUsers;
}
}
The code you're showing there only fills one function: it tells our trait to use the redirectPath from the AuthenticatesUsers trait rather than the one from RegistersUsers.
If you check inside the AuthenticatesUsers trait instead, you will find a getLogin() method. By default, this one is defined as
public function getLogin()
{
return view('auth.login');
}
All you have to do to get another view is then simply overwriting the function in your controller and returning another view. If you for some reason would like to load your views from a directory other than the standard resources/Views, you can do so by calling View::addLocation($path) (you'll find this defined in the Illuminate\View\FileViewFinder implementation of the Illuminate\View\ViewFinderInterface.
Also, please note that changing the auth views directory will do nothing to change the domain or similar. That is dependent on the function name (as per the definition of Route::Controller($uri, $controller, $names=[]). For more details on how routing works, I'd suggest just looking through Illuminate\Routing\Router.
for those who is using laravel 5.2, you only need to override property value of loginView
https://github.com/laravel/framework/blob/5.2/src/Illuminate/Foundation/Auth/AuthenticatesUsers.php
public function showLoginForm()
{
$view = property_exists($this, 'loginView')
? $this->loginView : 'auth.authenticate';
if (view()->exists($view)) {
return view($view);
}
return view('auth.login');
}
so to override the login view path, you only need to do this
class yourUserController {
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
.....
protected $loginView = 'your path';
}
I want to create a maintenance Page for my cake website by checking a Database Table for a maintenance flag using a sub-function of my AppController "initilize()" method. If the flag is set, i throw my custom MaintenanceException(Currently containing nothing special):
class MaintenanceException extends Exception{
}
To handle it, I implemented a custom App Exception Renderer:
class AppExceptionRenderer extends ExceptionRenderer {
public function maintenance($error)
{
return "MAINTENANCE";
}
}
I am able to see this maintenance Text on my website if I set my DB flag to true, but I could not find any information in cake's error handling documentation (http://book.cakephp.org/3.0/en/development/errors.html) on how I can actually tell the Exception renderer to render view "maintenance" with Template "infopage".
Can I even us that function using the ExceptionRenderer without a custom error controller? And If not, how should a proper ErrorController implementation look like? I already tried this:
class AppExceptionRenderer extends ExceptionRenderer {
protected function _getController(){
return new ErrorController();
}
public function maintenance($error)
{
return $this->_getController()->maintenanceAction();
}
}
together with:
class ErrorController extends Controller {
public function __construct($request = null, $response = null) {
parent::__construct($request, $response);
if (count(Router::extensions()) &&
!isset($this->RequestHandler)
) {
$this->loadComponent('RequestHandler');
}
$eventManager = $this->eventManager();
if (isset($this->Auth)) {
$eventManager->detach($this->Auth);
}
if (isset($this->Security)) {
$eventManager->detach($this->Security);
}
$this->viewPath = 'Error';
}
public function maintenanceAction(){
return $this->render('maintenance','infopage');
}
}
But this only throws NullPointerExceptions and a fatal error. I am really dissapointed by the cake manual as well, because the code examples there are nowhere close to give me an impression of how anything could be done and what functionality I actually have.
Because I had some more time today, I spent an hour digging into the cake Source and found a solution that works well for me (and is propably the way it should be done, altough the cake documentation does not really give a hint):
Step 1: Override the _template(...)-Method of the ExceptionRenderer in your own class. In my case, I copied the Method of the parent and added the following Code at the beginning of the method:
$isMaintenanceException = $exception instanceof MaintenanceException;
if($isMaintenanceException){
$template = 'maintenance';
return $this->template = $template;
}
This tells our Renderer, that the error Template called "maintentance"(which should be located in Folder: /Error) is the Error Page content it should render.
Step 2: The only thing we have to do now (And its is kinda hacky in my opinion, but proposed by the cake documentation in this exact way) is to set the layout param in our template to the name of the base layout we want to render with. So just add the following code on top of your error template:
$this->layout = "infopage";
The error controller I created is actually not even needed with this approach, and I still don't know how the cake error controller actually works. maybe I will dig into this if I have more time, but for the moment.
In my MVC 4 Controller, I want to override the View() method
ViewResult View(string viewName, string masterName, object model) {}
So that I can manipulate the view being rendered by the action method. To do this, I want to be able to obtain the physical path of the view file. I have tried the following:
string viewName = this.ControllerContext.RouteData.Route
.GetVirtualPath(this.ControllerContext.RequestContext, null)
.VirtualPath;
For example, this might return "/Errors/MissingParameters" when what I really want it to return is something like:
"~/Views/Errors/MissingParameters"
or, even better:
"~/Views/Errors/MissingParameters.cshtml"
Just to add complication, I also need it to cope with Areas, so if I had the same example running in an Area named "Surveys", I would want it to return something like:
"~/Areas/Surveys/Views/Errors/MissingParameters"
The reason I want to do this is that I'm experimenting with using views for globalization, so I might have two views:
"~/Views/Errors/MissingParameters.cshtml" // default view (en-GB)
"~/Views/Errors/MissingParameters_de-DE.cshtml" // German view (de-DE)
and I want to be able to check if the view exists for the current language/culture before referencing it.
Any advice would be much appreciated.
Thanks.
EDIT: This part will not work or is hard to implement
You'd rather use an action filter which will let you manipulate the Result before executing it.
Particularly you need a Result filter. Implement the IResultFilter.onResultExecuting method, and change the result there. Particularly when you implement this method:
void OnResultExecuting(ResultExecutingContext filterContext)
You can access the ResultExecutingContext.Result Property. This property will contain your view. If you cast it to System.Web.Mvc.ViewResultBase you'll have access to the ViewName and you'll be able to change it.
If you've never implemented a filter, this is a good hands-on-lab on the subject. In this case it implements another kind of filter, but it's just the same.
As an answer to the OP comment, it's perfectly normal that ViewName is missing, and View is still null. ViewName wouldn't be empty only if the case that the view is returned with name, like this: return View("Index");. And, the ViewName would be just, not the whole path to the view. So this is not a solution. So, to have this solution working you would have to deal with route data, controller context, etc. to find the view. (More on this below.)
EDIT: Solution, register a custom view engine
When MVC has to render a view it gets the information from the route data, the controller context, the view name (that, as explained above can be empty), and the conventions that apply.
Particularly, in MVC there is a collection of registered view engines which are required to find the view calling there FindView() method. The view engine will return a ViewEngineResult which has the found view, if one was found, or a list of the paths where the view has been unsuccesfully sought.
So, to modify the template path, you can override this funcionality: let the original class find the view, and, if it is found, modify the path.
To do show you need to take theses steps:
Inherit the view engine which you're using (my sampel code inherits Razor view engine)
Register your vie engine, so that it's queried before the original view engine (in my sample code I simply clear the registered engines list, and register mine. The original list includes razor and web form view engines)
This is the code for the inherited view engine:
public class CustomRazorViewEngine : FixedRazorViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
ViewEngineResult result
= base.FindView(controllerContext, viewName, masterName, useCache);
if (result.View != null)
{
// Modify here !!
}
return result;
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
ViewEngineResult result
= base.FindPartialView(controllerContext, partialViewName, useCache);
if (result.View != null)
{
// Modify here !!
}
return result;
}
static readonly PropertyInfo ViewPathProp
= typeof(RazorView).GetProperty("ViewPath");
public void SetViewPath(RazorView view, string path)
{
ViewPathProp.SetValue(view, path);
}
}
NOTE 1: where you read // Modify here !! you can modify the path property of the result.View. Cast it to RazorView: (result.View as RazorView).ViewPath. As the ViewPath setter is protected, you need to set it using Reflection: you can use the SetViewPath method for this.
NOTE 2: As you can see I'm not inheriting the RazorViewEngine but the FixedRazorViewEngine. If you loook for this class in MSDN you'll get not results, but if you look the original content of the registered view engines list, you'll find this class. I think this depends on an installed package in the project, and I think it solves a bug in MVC4. If you don't finf it in Microsoft.Web.Mvc namespace, inherit the original RazorViewEngined
NOTE 3: after the view is found, the view engine executes it, using the ViewEngineResult, so, if you change it, it will be executed with the new view path
And finally, you need to change the list of registered engines, in global.asax application start event, like this:
protected void Application_Start()
{
// Original content:
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
// Added content:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomRazorViewEngine());
}
NOTE: it would be cleaner, if you created a ViewEngineConfig class in App_Start folder, and invoked an static method of this class, just as it's done with all other configurations.
Answer was copied from here.
Well if you don't mind having your code tied to the specific view engine you're using, you can look at the ViewContext.View property and cast it to WebFormView
var viewPath = ((WebFormView)ViewContext.View).ViewPath;
I believe that will get you the view name at the end.
EDIT: Haacked is absolutely spot-on; to make things a bit neater I've wrapped the logic up in an extension method like so:
public static class IViewExtensions {
public static string GetWebFormViewName(this IView view) {
if (view is WebFormView) {
string viewUrl = ((WebFormView)view).ViewPath;
string viewFileName = viewUrl.Substring(viewUrl.LastIndexOf('/'));
string viewFileNameWithoutExtension = Path.GetFileNameWithoutExtension(viewFileName);
return (viewFileNameWithoutExtension);
} else {
throw (new InvalidOperationException("This view is not a WebFormView"));
}
}
}
which seems to do exactly what I was after.
Another solution here
((System.Web.Mvc.RazorView)htmlHelper.ViewContext.View).ViewPath
net-mvc
i am registering the area but i don't want my
url: "{area}/{controller}/{action}/{id}",
instead i want it to be like
url: "{controller}/{action}/{id}",
so i have registered my area like
context.MapRoute(
name: "AreaName_default",
url: "{controller}/{action}/{id}",
namespaces: new[] { "SolutionName.AreaName.Controllers" }
);
and i don't want to add the hard code string viewpath while returning view in every action method like
return View("~/Areas/AreaName/Views/ControllerName/ViewName.cshtml", model);
so i have created one result filter and override OnResultExecuting function
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
string areaName = AreaNameAreaRegistration.PropoertyName;
if (filterContext.Result.GetType() == typeof(ViewResult) || filterContext.Result.GetType() == typeof(PartialViewResult))
{
dynamic viewResult = filterContext.Result;
string viewname = string.IsNullOrEmpty(viewResult.ViewName) ? Convert.ToString(filterContext.RouteData.Values["action"]) : viewResult.ViewName;
string folder = Convert.ToString(filterContext.RouteData.Values["controller"]);
string lateralHireAreaViewPath = $"~/Areas/{areaName}/Views/";
string extension = viewname.Contains(".cshtml") ? "" : ".cshtml";
viewResult.ViewName = string.Concat(lateralHireAreaViewPath, folder, "/", viewname, extension);
ViewEngineResult result = ViewEngines.Engines.FindView(filterContext.Controller.ControllerContext, viewResult.ViewName, null);
if (result.View == null)
{
//searched in shared folder
lateralHireAreaViewPath = string.Concat(lateralHireAreaViewPath, "Shared/");
viewResult.ViewName = string.Concat(lateralHireAreaViewPath, "/", viewname, extension);
}
}
}
I am trying to control when a new view is created and when an existing view is shown.
This is a very similar scenario as outlined in the "Navigating to Existing Views" section in the Prism documentation, but I can't get it to work fully:
http://msdn.microsoft.com/en-us/library/gg430861(v=pandp.40).aspx
I am finding I can create the view/view model to begin with ok, but I am then unable to create a new instance of it. I.e. I want more than one instance to exist at once.
Here's an example of the view model:
[Export]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class DataEntryPageViewModel : INavigationAware, IRegionMemberLifetime
{
private Guid id;
[ImportingConstructor]
public DataEntryPageViewModel()
{
id = Guid.NewGuid();
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
// In actual fact there would be more logic here to determine
// whether this should be shown to the user
return false;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
}
public bool KeepAlive
{
// For the purposes of this example we don't want the view or the viewModel
// to be disposed of.
get { return true; }
}
}
I am navigating to this as follows:
m_RegionManager.RequestNavigate(
"MainRegion",
new Uri("/DataEntryPageView", UriKind.Relative));
So the first time I call the above the view is shown.
The next time I call RequestNavigate the IsNavigationTarget is hit and it returns false. What I then want it to do is to create a new instance but that doesn't happen. I know it's not happening because the constructor does not get hit and the UI does not update to show the new instance of the view.
Any ideas how I can make it create a new instance?
Many thanks,
Paul
Edit
I have noticed that the second time I call RequestNavigate (to request another instance of the same view) the callback reports an error "View already exists in region." It therefore seems that I can have multiple instances of different views in a region, but not multiple instances of the same view. My understand of this isn't great though so I could be wrong.
Why are you not creating the view when you want a new one to be created? It looks to me like you are using MEF.
Use the container to resolve a new instance of your view
Add the new instance of the view to the MainRegion
Then call Navigate and handle the appropriate logic in IsNavigationTarget
You should use the [Export] attribute in your view with a contract name: [Export("DataEntryPageView")].
I have now been able to get this to work, it was because I didn't have
[PartCreationPolicy(CreationPolicy.NonShared)]
on the class declaration of the view. I had it on the ViewModel.
So this is now resulting in the behaviour I expected.
Thanks though to Zabavsky and Alan for your suggestions.