Get resource value *without* using GetGlobalResourceObject - asp.net

I have an MVC web app that uses a custom folder & namespace for resource files. I need to be able to pull late-bound values from my resource file, i.e. using the resource type and string. I tried using:
HttpContext.GetGlobalResourceObject("ResourceClass", "SomeKey")
But that returns null. When I move the resource to App_GlobalResources, that works, but cascades into other issues. I tried working through those, but they seem to be deeper and more in number than going back to my original plan of just being able to read from the resource file in the custom folder.
In a nutshell, I'm trying to localize my xVal validation, both the error message, and in the case of a RegEx validator, the pattern. I've got everything working, except for this one piece, where I'm trying to localize the pattern. Since this isn't built into DataAnnotations.RegularExpressionAttribute, I need to fetch it myself, based on the resource type and name provided in the attribute. Hence my dilemma.
Can I get to the resource value using some other method? Or, must my resource file reside in the App_GlobalResources folder? If the latter, then I will need to open another discussion for all my other issues -- or implement some cruder form of localization for the regex stuff.
Thanks in advance.
Jerad

This is the solution we came up with:
public static class ResourceHelper
{
public static string GetString(Type resourceType, string resourceName)
{
return new ResourceManager(resourceType.FullName, resourceType.Assembly)
.GetString(resourceName);
}
public static string GetString(Type resourceType, string resourceName, CultureInfo culture)
{
return new ResourceManager(resourceType.FullName, resourceType.Assembly)
.GetString(resourceName, culture);
}
public static object GetObject(Type resourceType, string resourceName)
{
return new ResourceManager(resourceType.FullName, resourceType.Assembly)
.GetObject(resourceName);
}
public static object GetObject(Type resourceType, string resourceName, CultureInfo culture)
{
return new ResourceManager(resourceType.FullName, resourceType.Assembly)
.GetObject(resourceName, culture);
}
}

Related

Access .aspx page properties/variable from separate .cs file

Maybe a stupid question. C# 6.0 allows for string replacement using this syntax: $"string content {foo} {bar}". I would like to imitate this behavior in a class I've written for strings passed to it by default. The problem is that I am not sure how to access the public properties/variables. I am not sure if there is a way to access the properties using reflection or by passing this or this.Page to the constructor.
Figured it out:
public static object GetPropValue(object src, string propName)
{
return src.GetType().GetProperty(propName).GetValue(src, null);
}
object UID = GetPropValue(System.Web.HttpContext.Current.Handler, "UID");

How do QueryString parameters get bound to Action method parameters?

I have a webforms project, and am attempting to run some code that allows me to make a call to an MVC route and then render the result within the body of the web forms page.
There are a couple of HttpResponse/Request/Context wrappers which I use to execute a call to an MVC route, e.g.:
private static string RenderInternal(string path)
{
var responseWriter = new StringWriter();
var mvcResponse = new MvcPlayerHttpResponseWrapper(responseWriter, PageRenderer.CurrentPageId);
var mvcRequest = new MvcPlayerHttpRequestWrapper(Request, path);
var mvcContext = new MvcPlayerHttpContextWrapper(Context, mvcResponse, mvcRequest);
lock (HttpContext.Current)
{
new MvcHttpHandlerWrapper().PublicProcessRequest(mvcContext);
}
...
The code works fine for executing simple MVC routes, for e.g. "/Home/Index". But I can't specify any query string parameters (e.g. "/Home/Index?foo=bar") as they simply get ignored. I have tried to set the QueryString directly within the RequestWrapper instance, like so:
public class MvcPlayerHttpRequestWrapper : HttpRequestWrapper
{
private readonly string _path;
private readonly NameValueCollection query = new NameValueCollection();
public MvcPlayerHttpRequestWrapper(HttpRequest httpRequest, string path)
: base(httpRequest)
{
var parts = path.Split('?');
if (parts.Length > 1)
{
query = ExtractQueryString(parts[1]);
}
_path = parts[0];
}
public override string Path
{
get
{
return _path;
}
}
public override NameValueCollection QueryString
{
get
{
return query;
}
}
...
When debugging I can see the correct values are in the "request.QueryString", but the values never get bound to the method parameter.
Does anyone know how QueryString values are used and bound from an http request to an MVC controller action?
It seems like the handling of the QueryString value is more complex than I anticipated. I have a limited knowledge of the internals of the MVC Request pipeline.
I have been trying to research the internals myself and will continue to do so. If I find anything I will update this post appropriately.
I have also created a very simple web forms project containing only the code needed to produce this problem and have shared it via dropbox: https://www.dropbox.com/s/vi6erzw24813zq1/StackMvcGetQuestion.zip
The project simply contains one Default.aspx page, a Controller, and the MvcWrapper class used to render out the result of an MVC path. If you look at the Default.aspx.cs you will see a route path containing a querystring parameter is passed in, but it never binds against the parameter on the action.
As a quick reference, here are some extracts from that web project.
The controller:
public class HomeController : Controller
{
public ActionResult Index(string foo)
{
return Content(string.Format("<p>foo = {0}</p>", foo));
}
}
The Default.aspx page:
protected void Page_Load(object sender, EventArgs e)
{
string path = "/Home/Index?foo=baz";
divMvcOutput.InnerHtml = MvcWrapper.MvcPlayerFunctions.Render(path);
}
I have been struggling with this for quite a while now, so would appreciate any advice in any form. :)
MVC framework will try to fill the values of the parameters of the action method from the query string (and other available data such as posted form fields, etc.), that part you got right. The part you missed is that it does so by matching the name of the parameter with the value names passed in. So if you have a method MyMethod in Controller MyController with the signature:
public ActionResult MyMethod(string Path)
{
//Some code goes here
}
The query string (or one of the other sources of variables) must contain a variable named "Path" for the framework to be able to detect it. The query string should be /MyController/MyMethod?Path=Baz
Ok. This was a long debugging session :) and this will be a long response, so bear with me :)
First how MVC works. When you call an action method with input parameters, the framework will call a class called "DefaultModelBinder" that will try and provide a value for each basic type (int, long, etc.) and instance of complex types (objects). This model binder will depend on something called the ValueProvider collection to look for variable names in query string, submitted forms, etc. One of the ValueProviders that interests us the most is the QueryStringValueProvider. As you can guess, it gets the variables defined in the query string. Deep inside the framework, this class calls HttpContext.Current to retrieve the values of the query string instead of relying on the ones being passed to it. In your setup this is causing it to see the original request with localhost:xxxx/Default.aspx as the underlying request causing it to see an empty query string. In fact inside the Action method (Bar in your case) you can get the value this.QueryString["variable"] and it will have the right value.
I modified the Player.cs file to use a web client to make a call to an MVC application running in a separate copy of VS and it worked perfectly. So I suggest you run your mvc application separately and call into it and it should work fine.

Localization for "the value {0} is invalid" in case of int overflow

I've read answers about localization of validation errors by specifying DefaultModelBinder.ResourceClassKey, basically it's when entering string values in int field or not a datetime in datetime field.
But when I'm typing "111111111111111111111111111111" for an int field I get System.OverflowException and it looks like "The value '{0}' is invalid.".
Is there a way to localize (translate that message to other languages) that validation error in a way similar to other MVC-validation?
I had the same issue, and I finally managed to find the solution. Yes, that message can be localized, and luckily it's pretty easy when you figure it out.
You have to create a resource file and put it in the App_GlobalResources folder. You can call the file whatever you want, but I usually call it MvcValidationMessages.
Open the resource file and create a string with the name InvalidPropertyValue and write whatever message you want in the value field.
Now, open the Global.asax file and add the following line to the method Application_Start():
System.Web.Mvc.Html.ValidationExtensions.ResourceClassKey = "MvcValidationMessages";
"MvcValidationMessages" should of course be the correct name of the resource file you just created.
And voĆ­la! That's all there is to it. The message shown will now be your own instead of the default one.
I ended up overriding ModelBinder for int and supplying a localized error-message there:
public class IntModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
double parsedValue;
if (double.TryParse(value.AttemptedValue, out parsedValue))
{
if ((parsedValue < int.MinValue || parsedValue > int.MaxValue))
{
var error = "LOCALIZED ERROR MESSAGE FOR FIELD '{0}' HERE!!!";
bindingContext.ModelState.AddModelError(bindingContext.ModelName, string.Format(error, value.AttemptedValue, bindingContext.ModelMetadata.DisplayName));
}
}
return base.BindModel(controllerContext, bindingContext);
}
}
Then I simply registered it: ModelBinders.Binders.Add(typeof(int), new IntModelBinder()); and it now works fine.
P.S. sure, my localized errormessages are not hardcoded in model binder, this is just a simplified example :)

How to create an ASP.NET route that redirects to a path that uses part of the route?

I want to create a route that redirects all requests matching certain pattern to a location built using parts of the pattern. I want to grab some segment in the URL and treat the rest like a path to an aspx page in Web Forms application. For example
RouteTable.Routes.MapPageRoute("SomeRouteName", "{something}/{*path}", "~/pages/{*path}/Default.aspx");
Where *path could be something contain "\". The query string should be preserved as a query string.
Is it possible to create souch a route?
I don't know of any way to do it that way.
The more standard way would be to set the target as "~/pages/default.aspx" and then have that page check for the {path} argument and display the corresponding data.
If you really want it in another path, then don't use a {} placeholder. Simply hard code that section of the path (both the source and target).
After looking at several ways to do this I ended up creating my own routing handler that is something like this:
public class SomethingRoutingHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
string something = (string)requestContext.RouteData.Values["something"];
string path = (string)requestContext.RouteData.Values["path"];
string virtualPath = "~/" + path + "Default.aspx";
return BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(Page)) as Page;
}
}
I then use the RouteData in the pages to access something. I found these articles helpful:
http://msdn.microsoft.com/en-us/magazine/dd347546.aspx
http://www.xdevsoftware.com/blog/post/Default-Route-in-ASPNET-4-URL-Routing.aspx

How to use System.Web.Routing to not URL rewrite in Web Forms?

I am using System.Web.Routing with ASP.NET (3.5) Web Forms that will URL rewrite the following URL from
http://www.myurl.com/campaign/abc
to
http://www.myurl.com/default.aspx?campaign=abc
The code is as below:
public static void RegisterRoutes(RouteCollection routes)
{
routes.Add("CampaignRoute", new Route
(
"{campaign_code}",
new CustomRouteHandler("~/default.aspx")
));
}
IRouteHandler implementation:
public class CustomRouteHandler : IRouteHandler
{
public CustomRouteHandler(string virtualPath)
{
VirtualPath = virtualPath;
}
public string VirtualPath { get; private set; }
public IHttpHandler GetHttpHandler(RequestContext
requestContext)
{
if (requestContext.RouteData.Values.ContainsKey("campaign_code"))
{
var code = requestContext.RouteData.Values["campaign_code"].ToString();
HttpContext.Current.RewritePath(
string.Concat(
VirtualPath,
"?campaign=" + code));
}
var page = BuildManager.CreateInstanceFromVirtualPath
(VirtualPath, typeof(Page)) as IHttpHandler;
return page;
}
However I noticed there are too many things to change on my existing aspx pages (i.e. links to javascript, links to css files).
So I am thinking if there's a way to keep above code but in the end rather than a rewrite just do a Request.Redirect or Server.Transfer to minimize the changes needed. So the purpose of using System.Web.Routing becomes solely for URL friendly on the first entry.
How to ignore the rest of the patterns other than specificed in the code?
Thanks.
Using rewriting combined with ASP.NET URL Routing is not recommended because some implementations of ASP.NET URL Routing internally use rewriting as well (it depends on the version of ASP.NET). The combination of two different components using rewriting can cause conflicts (though I'm not 100% sure that that's why you're seeing this problem).
Regarding using transfer/redirect/rewrite:
My strongest recommendation would be to not use any of them! Instead of redirecting (or anything else) just let the page be called directly by ASP.NET by returning it from the IRouteHandler, much as you are already doing (just without the call to Rewrite). As long as your IRouteHandler saves the RouteData somewhere, the Page can then get the data from the route and you should be good to go.
Take a look at Phil Haack's Web Form routing sample to see an example of how to save the route data (or just use his code!).
Regarding ignoring patterns:
You can use an IRouteConstraint to constrain which URLs match your route. There is a built-in default route constraint implementation that uses regular expressions, but you can also write custom route constraints. Here is an example:
Route r = new Route(...);
r.Constraints = new RouteValueDictionary(new {
campaign_code = "\d{5}", // constrain to 5-digit numbers only
other_value = new CustomRouteConstraint(), // call custom constraint
});
CustomRouteConstraint is a class that you can write that derives from IRouteConstraint.
One thing I should note about static files such as CSS and JPG files is that by default they are always excluded from routing. By default routing ignores patterns that exactly match physical files on disk. You can change this behavior by setting RouteTable.Routes.RouteExistingFiles = true, but that is not the default.

Resources