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: <a href="mailto:@(Model.DirectContacts[i].Href)">@Model.DirectContacts[i].Href</a></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;
}
