I have an ASP.NET MVC application which has a view with a bunch of search criteria, and Search and Export buttons.
In order to make the site nice to use, and minimize the number of database hits, I'd like the users to be able to set the search criteria then hit either Search to get the results on screen (using an Ajax form) or Export directly to Excel. The key here is that both buttons are in an AJAX form in the view, and both perform a submit action so that the underlying actions in the controller receive the form fields.
I already have the exporting to Excel working - the file is created and written to a MemoryStream, and the action returns the memory stream. I have tested this using a separate button which doesn't do a submit and the file is returned correctly - however I do not have access to the parameters when doing a normal post.
Using this answer to another SO question, I have implemented multiple submit buttons and they both execute their respective actions correctly, and both get the form fields. However, because the action for the export file returns a MemoryStream, I receive an error as it is unable to parse the data (the page is expecting something which can be rendered by the browser).
I have adapted a work around for this which I found here, but it isn't one I'm very happy with due to the following flaws:
- Two requests are made when exporting: one to create the file, then another to retrieve the file.
- Clicking Export after doing a Search loses the search results (due to the InsertionMode set to replace). This will require another work around to swap the InsertionMode based on which button is clicked.
- Having to store the MemoryStream in the session (even if it is only temporary).
- Returning a script block from the Export action.
It generally feels like a bit of a hack, and I feel like there must be a tidier way to achieve the same result.
Here's a breakdown of the code I have:
My View (using bootstrap):
@using (Ajax.BeginForm("Summary", "MyController", null,
new AjaxOptions
{
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "result-scs",
OnBegin = "submitFormBegin(xhr, 'form-scs', '" + Url.Content("~/Content/Images/Loading.gif") + "')",
OnFailure = "submitFormFailure(xhr, status, error, 'form-scs', '" + Url.Action("Login", "Account") + "')",
OnComplete = "submitFormComplete(xhr, status, 'form-scs')"
},
new { id = "form-scs" }))
{
@Html.AntiForgeryToken()
<div class="row">
<div class="col-sm-3 form-group">
<label>Field1:</label>
@Html.DropDownList("field1", (IEnumerable<SelectListItem>)ViewBag.field1, "All", new { id = "", @class = "form-control input-sm multiselect" })
</div>
<div class="col-sm-3 form-group">
<label>Field2:</label>
@Html.ListBox("field2", (IEnumerable<SelectListItem>)ViewBag.field2, new { id = "", @class = "form-control input-sm multiselect" })
</div>
<div class="col-sm-3 form-group">
<label>Field3:</label>
@Html.ListBox("field3", (IEnumerable<SelectListItem>)ViewBag.field3, new { id = "", @class = "form-control input-sm multiselect" })
</div>
<div class="col-sm-3 form-group">
<label> </label>
<div class="btn-group btn-group-justified">
<div class="btn-group">
<button type="submit" value="Search" name="action:Search" class="btn btn-sm btn-primary" data-loading-text="Loading...">Search</button>
</div>
<div class="btn-group">
<button type="submit" value="Export" name="action:Export" class="btn btn-sm btn-primary" data-loading-text="Loading...">Export</button>
</div>
</div>
</div>
</div>
}
My Controller:
[ValidateAntiForgeryToken]
[MultiButton(MethodType="action", Argument="Search")]
public ActionResult Search(int? field1 = null,
int[] field2 = null, int[] field3 = null)
{
if (Request.IsAjaxRequest())
{
var model = Repository.List(field1, field2, field3);
return View(model);
}
else
{
throw new HttpException(404, "Resource unavailable.");
}
}
[MultiButton(MethodType = "action", Argument = "Export")]
public ActionResult Export(int? field1 = null,
int[] field2 = null, int[] field3 = null)
{
MemoryStream stream = Repository.GetExportStream(field1, field2, field3);
string filename = "Export" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".xlsx";
Session[filename] = stream;
return Content("<script>window.location = '/MyController/GetFile?filename=" + filename + "';</script>");
}
public ActionResult GetFile(string filename)
{
MemoryStream ms = (MemoryStream)Session[filename];
Session[filename] = null;
return File(ms, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Export.xlsx");
}
MultiButtonAttribute (to allow multiple buttons to submit to different actions):
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class MultiButtonAttribute : ActionNameSelectorAttribute
{
public string MethodType { get; set; }
public string Argument { get; set; }
public override bool IsValidName(ControllerContext controllerContext, string actionName, System.Reflection.MethodInfo methodInfo)
{
bool isValidName = false;
keyValue = String.Format("{0}:{1}", MethodType, Argument);
ValueProviderResult value = controllerContext.Controller.ValueProvider.GetValue(keyValue);
if (value != null)
{
controllerContext.Controller.ControllerContext.RouteData.Values[MethodType] = Argument;
isValidName = true;
}
return isValidName;
}
}
Is there are better way to achieve the same result? Maybe a way to obtain all the forms fields without doing a submit?