Can ASP.NET MVC 5 use Async methods? - asp.net-mvc

ASP.NET MVC 5 with .NET Framework v4.5.2
I've got a large, existing project that I have been assigned. Some parts of it take a really long time, so I wanted to see about putting some of these "new to me" asynchronous calls in the code.
But, after looking at the examples, I don't know if what I have can do that.
Take this typical method on my controller:
public ActionResult DealerSelect()
{
var model = RetailActivityModelData.GetCurrentDealers(null, ViewBag.Library);
return View(model);
}
I don't really understand how to turn that into an asynchronous call unless I did something like this:
public async Task<ActionResult> DealerSelect()
{
var model = await RetailActivityModelData.GetCurrentDealers(null, ViewBag.Library);
return View(model);
}
But the compiler complains with:
The return type of an async method must be void, Task, Task, a task-like type, IAsyncEnumerable, or IAsyncEnumerator
I'm not sure how to change the return type without destroying the View model.
So, I went into the GetCurrentDealers method to make a basic BackgroundWorker call:
public static DealerModel GetCurrentDealers(DealerModel model, string library)
{
m_library = library;
if (model == null)
{
model = new AdminCurrentDealerModel();
}
using (var bg = new BackgroundWorker())
{
var mre = new ManualResetEvent(false);
bg.DoWork += delegate (object s, DoWorkEventArgs e)
{
// fill 3 DataTables with calls to database
};
bg.ProgressChanged += delegate (object s, ProgressChangedEventArgs e)
{
// add data from DataTables to model
};
bg.RunWorkerCompleted += delegate (object s, RunWorkerCompletedEventArgs e)
{
mre.Set();
};
bg.RunWorkerAsync();
if (bg.IsBusy)
{
mre.WaitOne();
}
}
return model;
}
It compiles just fine, but crashes as soon as bg.RunWorkerAsync() is called:
System.InvalidOperationException: 'An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the Page is marked <%# Page Async="true" %>. This exception may also indicate an attempt to call an "async void" method, which is generally unsupported within ASP.NET request processing. Instead, the asynchronous method should return a Task, and the caller should await it.'
This is an ASP.NET MVC Razor page, so it doesn't really have that <%# Page Async="true" %> part.
Razor page:
#model Manufacturer.Models.CurrentDealerModel
#{
ViewBag.Title = "DealerSelect";
Layout = "~/Views/Shared/_ConsoleLayout.cshtml";
}
<style>
#DealerInfoGrid{
display:grid;
grid-template-columns:1fr 1fr 1fr;
}
</style>
<h2>Dealer Select</h2>
#{
var dealers = new List<SelectListItem>();
foreach (var dealer in Model.Dealers) {
dealers.Add(new SelectListItem() { Value = dealer.DealerNumber, Text = dealer.DealerName });
}
}
#using (Html.BeginForm())
{
#Html.AntiForgeryToken();
#Html.DropDownListFor(m => m.Dealer.DealerNumber,dealers);
<input type="submit" value="Submit" />
}
#{
if (Model.DealershipInfo != null)
{
<div id="DealerInfoGrid">
<div>
<label>Region: #Model.DealershipInfo.Region</label><br />
<label>Name: #Model.DealershipInfo.Name</label><br />
<label>Address: #Model.DealershipInfo.Address</label><br />
</div>
<div>
<label>Dealer No: #Model.DealershipInfo.DealerNumber</label><br />
<label>Sales Phone: #Model.DealershipInfo.SalesPHNumber</label>
</div>
</div>
}
if(Model.Contacts.Count() >0)
{
<table>
<tr>
<td>Title</td>
<td>Contact Name</td>
<td>Digital Contact</td>
<td>Phone Type</td>
<td>Phone Number</td>
</tr>
#foreach (var contact in Model.Contacts)
{
<tr>
<td>
#contact.Title
</td>
<td>
#contact.ContactName
</td>
<td>
#contact.DigitalContact
</td>
<td>
#contact.PhoneType
</td>
<td>
#contact.PhoneNumber
</td>
</tr>
}
</table>
}
if(#Model.DirectContacts.Count >0)
{
for (var i = 0; i < Model.DirectContacts.Count(); i++)
{
<label>#Model.DirectContacts[i].Department: #Model.DirectContacts[i].Href</label><br />
}
}
}
Can I even do incremental asynchronous development on this project, or does the whole thing need to be converted to one that uses Tasks?
Edit: Solved
I wound up going with a hybrid approach that combined the 2 answers that I received. From what Stephen wrote, I abandoned the BackgroundWorker control that I was familiar with, and like Ronnie pointed out, I needed to make the call to GetCurrentDealers asynchronous as well.
public static async Task<CurrentDealerModel> GetCurrentDealersAsync(CurrentDealerModel model)
{
bool includeDealerDetails = true;
if (model == null)
{
includeDealerDetails = false;
model = new CurrentDealerModel();
}
using (var conn = new OleDbConnection(m_conStr))
{
conn.Open();
var tasks = new List<Task>();
var taskGetDealerInfo = GetDealerInfoAsync(conn);
var taskGetDealershipInfo = GetDealershipInfoAsync(conn);
var taskGetContacts = GetContactsAsync(conn);
var taskGetDirectContacts = GetDirectContactsAsync(conn);
tasks.Add(taskGetDealerInfo);
if (includeDealerDetails)
{
tasks.Add(taskGetDealershipInfo);
tasks.Add(taskGetContacts);
tasks.Add(taskGetDirectContacts);
}
while (0 < tasks.Count)
{
var done = await Task.WhenAny(tasks);
if (!done.IsFaulted)
{
if (done == taskGetDealerInfo)
{
model.Dealers = taskGetDealerInfo.Result;
}
else if (done == taskGetDealershipInfo)
{
model.AllDealerships = taskGetDealershipInfo.Result;
}
else if (done == taskGetContacts)
{
model.Contacts = taskGetContacts.Result;
}
else if (done == taskGetDirectContacts)
{
model.DirectContacts = taskGetDirectContacts.Result;
}
} else
{
throw done.Exception.Flatten();
}
tasks.Remove(done);
}
}
return model;
}
Each of the tasks are small and basic, like this one:
private static async Task<DealerSelectInfoList> GetDealerInfoAsync(OleDbConnection openConnection)
{
var result = new DealerSelectInfoList();
var table = new DataTable();
using (var cmd = new OleDbCommand("", openConnection))
{
cmd.CommandText = $#"
SELECT DISTINCT(DADNAM) AS DADNAM, DADLRN, DASRGN
FROM {m_library}.DLRINVAPF
WHERE DASRGN IN(SELECT RGNAMEM FROM {m_library}.REGIONPF)
ORDER BY DADNAM ASC for read only with UR";
var reader = await cmd.ExecuteReaderAsync();
table.Load(reader);
}
foreach (DataRow row in table.Rows)
{
result.Add(new DealerSelectInfo()
{
DealerName = $"{row["DADNAM"]}".Trim(),
DealerNumber = $"{row["DADLRN"]}".Trim(),
});
}
return result;
}

Your method GetCurrentDealers() isn't async so you can't call it with await.
Edit
Controller
public async Task<IActionResult> DealerSelect()
{
var model = await RetailActivityModelData.GetCurrentDealersAsync(null, ViewBag.Library);
return View(model);
}
GetCurrentDealersAsync()
public static async Task<DealerModel> GetCurrentDealersAsync(DealerModel model, string library)
{
m_library = library;
if (model == null)
{
model = new AdminCurrentDealerModel();
}
using (var bg = new BackgroundWorker())
{
var mre = new ManualResetEvent(false);
bg.DoWork += delegate (object s, DoWorkEventArgs e)
{
// fill 3 DataTables with calls to database
};
bg.ProgressChanged += delegate (object s, ProgressChangedEventArgs e)
{
// add data from DataTables to model
};
bg.RunWorkerCompleted += delegate (object s, RunWorkerCompletedEventArgs e)
{
mre.Set();
};
bg.RunWorkerAsync();
if (bg.IsBusy)
{
mre.WaitOne();
}
}
return model;
}

ASP.NET MVC 5 with .NET Framework v4.5.2
Please see my article Introduction to Async on ASP.NET. In particular, ensure you have httpRuntime.targetFramework set correctly to avoid the server-side "quirks mode".
make a basic BackgroundWorker call
You should never use BackgroundWorker on ASP.NET. And almost never use Task.Run (or StartNew). You cannot "make something asynchronous" this way.
The exception you encounter is a "safety net" exception.
I don't really understand how to turn that into an asynchronous call
Don't start at the controllers. That's actually the opposite of the easiest way to do it. Instead, start with the lowest-level code you have that performs any I/O. E.g., database queries or calls to external APIs. One at a time, make those asynchronous (using await to call asynchronous APIs), and then allow the async/await usage to grow out from there. Eventually, your controller methods will become async as the last step of this process.
Can I even do incremental asynchronous development on this project, or does the whole thing need to be converted to one that uses Tasks?
As you're converting to async, you may find it useful to (temporarily) have two copies of the code: one async (used by code that is already converted) and one sync (used by unconverted code). This is fine if you're doing an conversion-to-async that won't be interrupted by other feature work. If you need longer-term support for both async and sync code, then you can use the boolean argument hack described in my Brownfield Async article (or if you want a more modern/complex solution, use generic code generation as described on my blog).

Related

Syncfusion Asp.Net Datagrid: How to return error from CRUD class

I have an Syncfusion Asp.net grid performing CRUD operations.
public ActionResult Insert(CRUDModel<Source> newItem)
{
using (var context = new ImageStormEntities())
{
context.Sources.Add(newItem.Value);
context.SaveChanges();
}
return Json(newItem.Value);
}
cshtml:
#Html.EJS().Grid("DataGrid").DataSource(ds => ds.Json(ViewBag.datasource).UpdateUrl("/Management/Update").InsertUrl("/Management/Insert").RemoveUrl("/Management/Remove").Adaptor("RemoteSaveAdaptor")).Columns(col =>
{
col.Field("id").IsPrimaryKey(true).Visible(false).Add();
col.Field("ResourceGroup").HeaderText("Source VM Resource Group").Add();
col.Field("VMName").HeaderText("Source VM Name").Add();
col.Field("imageVersion").HeaderText("Image Version").Add();
}).ActionFailure("OnActionFailure").AllowTextWrap(true).TextWrapSettings(text => { text.WrapMode(Syncfusion.EJ2.Grids.WrapMode.Header); }).AllowPaging().FilterSettings(filter => { filter.Type(Syncfusion.EJ2.Grids.FilterType.Menu); }).EditSettings(edit => { edit.AllowAdding(true).AllowEditing(true).AllowDeleting(true).ShowDeleteConfirmDialog(true).Mode(Syncfusion.EJ2.Grids.EditMode.Dialog); }).Toolbar(new List<string>
() { "Add", "Edit", "Delete", "Update", "Cancel" }).Render()
On the page, I have:
<script>
function OnActionFailure(args) {
alert(args.error.status + " : " + args.error.statusText);
}
</script>
That returns a simple toast message when values break the db constraints, but values are empty.
I want to send usefull info to user.
I can catch the error, what do I return to make this work.
Also, I would rather the use got a Messagebox to discharge rather that a toast.
Changed your controllers code this will fix your issue
To
public ActionResult Insert(CRUDModel<Source> newItem)
{
using (var context = new ImageStormEntities())
{
if(newItem.Value! = null)
{
context.Sources.Add(newItem.Value);
context.SaveChanges();
}
else
{
throw new Exception("Empty values cannot be inserted");//Add custom
exception message
}
}
return Json(newItem.Value);
}
and in your js add lime this
<script>
function OnActionFailure(args) {
var errorMessage = args[0].error.responseText.split("Exception:")[1].split('<br>')[0]; //extract the message from args
alert(errorMessage);
//you will get exact error that you have returned from serverside in errorMessage
}
</script>

How to list all contacts with Microsoft Graph?

I am experimenting with the Microsoft Graph API and I set up a project with the QuickStarts on the Microsoft developers site.
I'm currently having problems when trying to list all email contacts associated with the logged account.
My problem seems to reside on how I'm requesting and passing the information to ViewBag as it throws a NullReference exceptions even with the proper front-end listing.
Here is the relevant code:
HomeController.cs
public async Task<ActionResult> GetMyContacts()
{
try
{
// Init GraphServiceClient
GraphServiceClient graphClient = SDKHelper.GetAuthenticatedClient();
var contacts = await graphService.GetMyEmailContacts(graphClient);
ViewBag.Contacts = contacts;
return View("Graph");
}
catch (ServiceException se)
{
if (se.Error.Message == Resource.Error_AuthChallengeNeeded) return new EmptyResult();
return RedirectToAction("Index", "Error", new { message = Resource.Error_Message + Request.RawUrl + ": " + se.Error.Message });
}
}
GraphService.cs
public async Task<IList<Contact>> GetMyEmailContacts(GraphServiceClient graphClient)
{
User me = await graphClient.Me.Request().Select("mail,userPrincipalName").GetAsync();
var contacts = me.Contacts;
return contacts;
}
Graph.cshtml
<div class="col-sm-12">
<pre name="user-contacts">#ViewBag.Contacts</pre>
</div>
<h2>TEST GET CONTACTS</h2>
<div class="col-sm-12">
<p>#Html.Raw(ViewBag.Contacts)</p>
</div>
What am I missing here?
In your view, you have to cast it back to the original type. ViewBag is a C# 4 dynamic type. Returned entities are also dynamic unless cast.
var contactList = ViewBag.Contacts as IEnumerable<Contact>;
If you want to display the whole list, it's as simple as this:
#foreach (var contact in contactList)
{
<li>#contact</li>
}

MVC - Html.BeginForm. My action method does not recieve argument

I am trying to save data to my model.
Here is my Action method for doing that:
...
[HttpPost]
public ActionResult UpdateSectionInformation(SectionInformation sectionInformation) {
ViewBag.SectionUpdatedSuccesfully = true;
SectionInformation s = sectionInformation;
// Save settings to database.
using (var session = MvkStorage.GetSession()) {
var client = new MvkSectionStorageHandler(session);
try {
client.SaveOrUpdateSection(s.Section);
client.SaveOrUpdateSectionReschedulingRelations(s.RescheduleFromRelations);
client.SaveOrUpdateSectionReschedulingRelations(s.RescheduleToRelations);
} catch (Exception) {
ViewBag.SectionUpdatedSuccesfully = false;
}
}
return View("Index", s);
}
Here is the Html.BefinForm "declaration" (the controller name is SectionController):
#using(Html.BeginForm("UpdateSectionInformation", "Section", FormMethod.Post)) {
It seems like the problem is that the UpdateSectionInformation get an empty model. How should I solve that? I am new to MVC and I really donĀ“t get where UpdateSectionInformation is called.
I have seen something about Html.HiddenFor, should I use that here?:
#for (int i = 0; i < Model.RescheduleFromRelations.Count(); i++ ) {
var relation = Model.RescheduleFromRelations.ElementAt(i);
var relationId = relation.FromSection.ToString() + relation.ToSection.ToString();
...
Edit: Problem solved thanks to comment

Data does not refresh in browser but does in database

I have a working submit button, so when I make changes to my form and click submit the form updates to the changes...
HOWEVER. In the database it shows the new data. But when I RedirectToAction to the last page it doesn't show the updated data.
This is my code (Controller):
[HttpPost]
public ActionResult Save(M2Portal.Areas.Admin.Models.Users.Roles roleForm)
{
try
{
if (ModelState.IsValid)
{
var role = Srvctx.Roles.FirstOrDefault(w => w.RoleID == roleForm.RoleId);
role.RoleDescription = roleForm.RoleDescription;
Srvctx.SubmitChanges();
return RedirectToAction("RoleManagement");
}
return RedirectToAction("RoleManagement");
}
catch
{
return RedirectToAction("RoleManagement");
}
}
so when it hits:
return RedirectToAction("RoleManagement");
it just goes to the page but doesnt refresh the data. but when i look at my database its been changed there.
Am new to ASP.NET MVC and have no idea where to start looking...
Any ideas?
role mangagement:
public ActionResult RoleManagement(string userName)
{
return View("RoleManagement", new UserForm(userName));
}
Rolemangement cshtml:
#foreach (M2DAL.M2Service.Role r in Model.AllRoleIDs)
{
<tr>
<td>#r.RoleName</td> //This is not refreshing, on debug shows the number, but browser shows old number)
<td>#r.RoleID</td>
<td>#Html.ActionLink("Edit Roles", "EditRole", "Users", new { roleId = r.RoleID }, new { #class = "action" }) </td>
</tr>
}
You didn't post the code for RoleManagement action. I don't know where are you storing your model, but are you sure, that this action is reading the DB again ?
There should be something like (just an example)
[HttpGet]
public ActionResult RoleManagement()
{
var roles = Srvctx.Roles.Take(1000);
return roles;
}

Get the Current ViewContext in ASP.Net MVC

I have a ASP.Net MVC JsonResult function in which I want to return the contents of a PartialView (The content has to be loaded using Ajax, and for some reason I can't return a PartialViewResult).
To render the PartialView I need the ViewContext object.
How do you get the current ViewContext object within an Action method? I don't even see HttpContext.Current in my action method.
I am using ASP.net MVC 1.
a ViewContext is not available within the action method because it is constructed later before rendering the view. I would suggest you using MVCContrib's BlockRenderer to render the contents of a partial view into a string.
I may have missed a point somewhere but my Actions that returned partial views do so by returning a View object that refers to an ascx page. This will return partial HTML without the full page constructs (html, head, body, etc.). Not sure why you'd want to do anything beyond that, is there a specific reason you need to return PartialViewResult? Here's an example from my working code.
First the Action in my controller:
public ViewResult GetPrincipleList(string id)
{
if (id.Length > 1)
id = id.Substring(0, 1);
var Principles = competitorRepository.Principles.Where(p => p.NaturalKey.StartsWith(id)).Select(p=>p);
return View(Principles);
}
And then the partial view (ascx):
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<MyProject.Data.Principle>>" %>
<% foreach (var item in Model) { %>
<div class="principleTitle" title="<%= Html.Encode(item.NaturalKey) %>"><%= Html.Encode(item.Title) %></div>
<%} %>
Lastly, the Jquery that sets up the call:
$(function() {
$(".letterSelector").click(function() {
$("#principleList").load("/GetPrincipleList/" + $(this).attr("title"), null, setListClicks);
});
});
So, a full AJAX process, hope that helps.
---- UPDATE following comment ----
Returning Json data is just as easy:
Firstly, initiating the AJAX call when a select box changes:
$("#users").change(function() {
var url = "/Series/GetUserInfo/" + $("#users option:selected").attr("value");
$.post(url, null, function(data) { UpdateDisplay(data); }, 'json');
});
The javascript that processes the returned json data:
function UpdateDisplay(data) {
if (data != null) {
$("div.Message").fadeOut("slow", function() { $("div.Message").remove(); });
$("#Firstname").val(data.Firstname);
$("#Lastname").val(data.Lastname);
$("#List").val(data.List);
$("#Biography").val(data.Biography);
if (data.ImageID == null) {
$("#Photo").attr({ src: "/Content/Images/nophoto.png" });
$("#ImageID").val("");
}
else {
if (data.Image.OnDisk) {
$("#Photo").attr({ src: data.Image.ImagePath });
}
else {
$("#Photo").attr({ src: "/Series/GetImage?ImageID=" + data.ImageID });
}
$("#ImageID").val(data.ImageID);
}
$("form[action*='UpdateUser']").show();
} else {
$("form[action*='UpdateUser']").hide();
}
};
And finally the Action itself that returns the json data:
public JsonResult GetUserInfo(Guid id)
{
MyUser myuser = (from u in seriesRepository.Users
where u.LoginID == id
select u).FirstOrDefault();
if (myuser == null)
{
myuser = new MyUser();
myuser.UserID = 0;
myuser.Firstname = Membership.GetUser(id).UserName;
myuser.Lastname = "";
myuser.List = "";
myuser.Biography = "No yet completed";
myuser.LoginID = id;
}
return Json(myuser);
}
Does that help? If not then can you post some of the code you are working on as I'm missing something.

Resources