I need to run an AJAX method from one of the pages in my webflow. According to the docs, this is what RequestContextHolder is for.
So, here is my method:
#RequestMapping(value="training/test", method=RequestMethod.POST)
public GridItem getGridItems() {
RequestContext requestContext = RequestContextHolder.getRequestContext();
Set<String> fsKeyset = requestContext.getFlowScope().asMap().keySet();
for (String key: fsKeyset) {
log.debug(key);
}
Form form = (Form) requestContext.getFlowScope().get("form");
return form.getGridItem();
}
Unfortunately, the RequestContext is null. Is there something special I need to do to actually GET the RequestContext?
Jason
Lets say your flow/view definition look like this
<view id="someId" view="someView" model="entity">
<transition on="ajaxSave" to="handleAjaxActionState"/>
<transition on="save" to="handleStandardSave"/>
</view>
then the bare min for your ajax url would look like:
url=${flowExecutionUrl}&_eventId=ajaxSave&ajaxSource=true
executing this (while you are still inside "someId" view state) will then get picked up in the "ajaxSave" transition.
disclaimer: I did not test the above example.
Related
I have a wpf application with a TextBox bound to ActualPageNumber property in the VM. I also have a DataGrid bound to an ObservableCollection which displays the given page. The data are stored in DB. When I change the ActualPageNumber, the setter accesses the db which can be slow. That is why I wanted an async setter, to keep the gui responsive.
I understand there is no async setter: https://blog.stephencleary.com/2013/01/async-oop-3-properties.html
I also found useful stuff like https://stackoverflow.com/a/9343733/5852947, https://stackoverflow.com/a/13735418/5852947, https://nmilcoff.com/2017/07/10/stop-toggling-isbusy-with-notifytask/
Still I struggle how to go on this case. AsyncEx library can be the solution, an example would be nice.
I just would like to notify the user that the page is actually loading. If I could call async from the setter I could do it, but then I still can not use await in the setter because it is not async.
I also have a DataGrid bound to an ObservableCollection which displays the given page.
This is going to be the difficult part. DataGrid (and DataTable and friends) are designed with a synchronous API, and have never been updated to support asynchrony.
I'm not terribly familiar with DataGrid, but I'd say your options are:
Replace the DataGrid with your own custom control - say, a ListView that displays custom controls. Then you can display a loading spinner since you control the custom control. There are some common patterns for this like NotifyTask.
There might be a way to virtualize the data in the DataGrid in a way that it would asynchronously load. I'm not familiar enough with DataGrid to say whether this is actually possible, but it's worth looking into.
1) For the responsiveness of the DataGrid, this binding property might help: IsAsync=True
<DataGrid ItemsSource="{Binding MyCollection, IsAsync=True}"
also look into these DataGrid properties:
VirtualizingPanel.IsVirtualizing
VirtualizingPanel.VirtualizationMode (you'll probably need Recycling)
VirtualizingPanel.IsVirtualizingWhenGrouping
EnableRowVirtualization
EnableColumnVirtualization
But be careful, virtualization can play tricks on you. For example, I had a RowHeader (with the row number) and the values got scrambled when virtualization was on.
2) About the async setter for data binding: I was using a custom version of IAsyncCommand (see Stephen Cleary's example).
I used the command in 2 ways: a) binding to it from the view (avoiding the async setter altogether) or b) launching it from the setter (not nice).
Example: I created an UpdateCommand as an AsyncCommand and placed everything I needed done asynchronously (like getting the values from the DB). Everything in this command is wrapped within a display+hide of a "in progress"-like control - in my case, a transparent cover with a spinner + "please wait...", to prevent other user actions (the "screen" is visible while the task is performed). Stripped down sample:
....
public MainWindowViewModel()
{
UpdateCommand = AsyncCommand.Create(Update); // our own custom implementation of AsyncCommand
}
....
public AsyncCommand UpdateCommand { get; }
internal async Task Update(object arg)
{
await SafeWrapWithWaitingScreenAsync(async () =>
{
var value = (int)arg; // or the ActualPageNumber, if used from a1)
var data = await GetDataFromDb(value).ConfigureAwait(false);
...// fill in MyCollection (which is the DataGrid's ItemsSource) using the data
OnPropertyChanged(nameof(MyCollection));// if still needed
}).ConfigureAwait(false);
}
....
public async Task SafeWrapWithWaitingScreenAsync(Func<Task> action)
{
DisplayWaitingScreen = true; //Visibility of the "Waiting screen" binds to this
try
{
await action().ConfigureAwait(false);
}
catch (Exception ex)
{
HandleException(ex); // display/log ex
}
finally
{
DisplayWaitingScreen = false;
}
}
a) Binding to the command from the view and
a1) in the command's body use ActualPageNumber property instead of the arg value
or a2) passing a CommandParameter which binds to the same property as TextBox.Text does. Example (could be missing something, couse is not the real code):
<TextBox Text="{Binding ActualPageNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<TextBox.InputBindings>
<KeyBinding Key="Return" Command="{Binding UpdateCommand}" CommandParameter="{Binding ActualPageNumber}" />
<KeyBinding Key="Enter" Command="{Binding UpdateCommand}" CommandParameter="{Binding ActualPageNumber}" />
</TextBox.InputBindings>
</TextBox>
b) Not sure this is right, but before seeing Stephen's approach with NotifyTaskCompletion<TResult> (which I will probably use in the future), for the setter, I launched the command something like:
private int actualPageNumber;
public int ActualPageNumber
{
get => actualPageNumber;
set
{
actualPageNumber = value;
OnPropertyChanged(); //the sync way
UpdateCommand.Execute(value);
}
}
It seems that in ASP.NET Core, the value in asp-* attributes (e.g. asp-for) is taken from the request payload before the model. Example:
Post this value:
MyProperty="User entered value."
To this action:
[HttpPost]
public IActionResult Foo(MyModel m)
{
m.MyProperty = "Change it to this!";
return View();
}
OR this action
[HttpPost]
public IActionResult Foo(MyModel m)
{
m.MyProperty = "Change it to this!";
return View(m);
}
View renders this:
<input asp-for="MyProperty" />
The value in the form input is User entered value. and not Change it to this!.
First of all, I'm surprised that we don't need to pass the model to the view and it works. Secondly, I'm shocked that the request payload takes precedence over the model that's passed into the view. Anyone know what the rationale is for this design decision? Is there a way to override the user entered value when using asp-for attributes?
I believe this is the expected behavior/by design. Because when you submit the form, the form data will be stored to ModelState dictionary and when razor renders your form elements, it will use the values from the Model state dictionary. That is why you are seeing your form element values even when you are not passing an object of your view model to the View() method.
If you want to update the input values, you need to explcitly clear the Model state dictionary. You can use ModelState.Clear() method to do so.
[HttpPost]
public IActionResult Create(YourviewModel model)
{
ModelState.Clear();
model.YourProperty = "New Value";
return View(model);
}
The reason it uses Model state dictionary to render the form element values is to support use cases like, showing the previously submitted values in the form when there is a validation error occurs.
EDIT : I found a link to the official github repo of aspnet mvc where this is confirmed by Eilon (asp.net team member)
https://github.com/aspnet/Mvc/issues/4486#issuecomment-210603605
I can confirm your observation. What's really going to blow your mind is that this:
[HttpPost]
public IActionResult Foo (MyModel m)
{
m.MyProperty = "changed";
var result = new MyModel { MyProperty = "changed" };
return View(result);
}
...gives you the same result.
I think you should log a bug: https://github.com/aspnet/mvc/issues
Edit: I now remember this issue from previous encounters myself and concede that it isn't necessarily a bug, but rather an unintended consequence. The reasons for the result of executing this code is not obvious. There likely isn't a non-trivial way to surface a warning about this, but PRG is a good pattern to follow.
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.
I am trying to write a module/areas based mvc3 CMS using razor view engine. I have two layout views, _site.cshtml and _modules.cshtml. The _site.cshtml has an #RenderBody() section. My application always calls a view called "index.cshtml" which has its layout set to _site.cshtml page.
The Problem: The problem is that my modules/areas are rendered before doctype element - and not inside the RenderBody section of layout page.
It looks like what you're trying to do is the equivalent of Html.RenderAction (or Html.Action), so why are you writing all that code yourself instead of using the built-in functionality?
In short, its your Invoke() method! The razor engine works by writing to an in memory stream, and then parses it to the response. The reason you are getting that behaviour is because your Invoke() method is writing straight to the response stream; i.e. before the in memory stream is parsed.
I came across similar behaviour when using the Html.RenderAction(), which pointed to an action that returned a PartialView. The workaround was to use Html.Action(). The difference being is that Action() returns a string which gets appended to the in memory stream, and RenderAction() writes to the directly to the response.
If you post the code for your Invoke() method, I may be able to help you futher!
| -- Edit -- |
OK, this turned out to be more complex that initially anticipated. The problem is that I could not get ProcessRequest() to append to the current response; however, I may have a solution.
public string ProcessRoute(ViewContext viewContext, RouteData routeData)
{
var urlHelper = new UrlHelper(viewContext.RequestContext);
var currentUrl = urlHelper.Action(routeData.Values["action"].ToString(),
routeData.Values["controller"].ToString(), routeData.DataTokens);
var stringWriter = new StringWriter();
var simpleWorkerRequest = new SimpleWorkerRequest(currentUrl, "", stringWriter);
var context = new HttpContext(simpleWorkerRequest);
var contextBase = new HttpContextWrapper(context);
var requestContext = new RequestContext(contextBase, routeData);
var httpHandler = routeData.RouteHandler.GetHttpHandler(requestContext);
httpHandler.ProcessRequest(context);
context.Response.End();
stringWriter.Flush();
return stringWriter.GetStringBuilder().ToString();
}
The code above generates a new Request and returns the Request's HTML as a string. By doing this, the result is appended as part of the current response. You can now rewrite your Invoke() function to return a string, which can be displayed on your View.
public string Invoke(ViewContext viewContext)
{
if (_mvcHandler == null)
{
var routeData = new RouteData(context.RouteData.Route,
context.RouteData.RouteHandler);
routeData.Values.Add("id", _id);
routeData.Values.Add("moduleName", _moduleName);
routeData.Values.Add("controller", _controllerName);
routeData.Values.Add("action", _actionName);
routeData.Values.Add("pageContext", _pageContext);
if (!string.IsNullOrEmpty(_preferredNamespace))
{
routeData.DataTokens.Add("Namespaces", new[] { _preferredNamespace });
}
return ProcessRoute(viewContext, routeData);
}
return string.Empty;
}
You may also have to change;
mr.Invoke(ViewContext);
To;
Html.Raw(mr.Invoke(ViewContext));
In order stop the HTML encoding behaviour.
| -- Note -- |
Since I don't have your ModuleRequests class, I couldn't test this code specifically for your scenario. Instead, I replicated the problem as best as I could and solved it.
Please let me know if you have any questions.
Matt
Let me post a plausible second alternative to your methods while I try and solve the problem above.
There is a html extension in MVC called Html.Partial(), which allows you to use routedata to return the result of a controller action.
So, if you had an AccountController with an Register method that returned a PartialView result, then it would be appended to the current page.
public class AccountController() : Controller
{
public ActionResult Register()
{
return PartialView();
}
}
The call to this action could look something like;
#Html.Partial("Account", "Register");
| -- Note -- |
Don't use #Html.RenderPartial(), or you will have the same problem as above!
I am currently posting away from a computer, so I can't test this theory!
Hope this helps!
Matt
The default route in MVC {controller}/{action}/{id} is for the most part quite helpful as is being able to set a default if the incoming url doesn't include a parameter but is there also a way to specify a default action for when an action doesn't exist on a controller?
What I want to achieve is being able to have controllers with several specific actions and then its own catchall which uses the url to grab content from a basic CMS.
For example a products controller would be something like:
public class ProductsController: Controller{
public ActionResult ProductInfo(int id){...}
public ActionResult AddProduct(){...}
public ActionResult ContentFromCms(string url){...}
}
Where the default route would handle /Products/ProductInfo/54 etc but a request url of /Products/Suppliers/Acme would return ContentFromCms("Suppliers/Acme"); (sending the url as a parameter would be nicer but not needed and a parameterless method where I get it from Request would be fine).
Currently I can think of two possible ways to achieve this, either:
Create a new constraint which reflects over a controller to see if it does have an action of a given name and use this in the {controller}/{action}/{id} route thus allowing me to have a more general catchall like {controller}/{*url}.
Override HandleUnknownAction on the controller.
The first approach seems like it would be quite a roundabout way of checking this while for the second I don't know the internals of MVC and Routing well enough to know how to proceed.
Update
There's not been any replies but I thought I'd leave my solution incase anyone finds this in future or for people to suggest improvements/better ways
For the controllers I that wanted to have their own catchall I gave them an interface
interface IHasDefaultController
{
public string DefaultRouteName { get; }
System.Web.Mvc.ActionResult DefaultAction();
}
I then derived from the ControllerActionInvoker and overrode FindAction. This calls the base FindAction then, if the base returns null and the controller impliments the interface I call FindAction again with the default actionname.
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
ActionDescriptor foundAction = base.FindAction(controllerContext, controllerDescriptor, actionName);
if (foundAction == null && controllerDescriptor.ControllerType.GetInterface("Kingsweb.Controllers.IWikiController") != null)
{
foundAction = base.FindAction(controllerContext, controllerDescriptor, "WikiPage");
}
return foundAction;
}
As I also want parameters from the routing I also replace the RouteData at the start of the default Actionresult on the controller
ControllerContext.RouteData = Url.RouteCollection[DefaultRouteName].GetRouteData(HttpContext);
You approach is quite fine. As a side-note:
replace
controllerDescriptor.ControllerType.GetInterface("Kingsweb.Controllers.IWikiController") != null
with
typeof(Kingsweb.Controllers.IWikiController).IsAssignableFrom(controllerDescriptor.ControllerType)
this is more strongly-typed way then passing in the name of the interface via string: what if you change the namespace tomorrow?..