Anthony
Anthony

Reputation: 2824

When should an action be marked as async?

I have a controller action in mvc 4 app:

public ActionResult Index()
    {
        GetStartedModel gsModel = new GetStartedModel();

        return View(gsModel);
    }

and ViewModel:

public class GetStartedModel
{
    public IEnumerable<SelectListItem> listA { get; set; }
    public IEnumerable<SelectListItem> listB { get; set; }

    public GetStartedModel()
    {
        TestDataWebServiceHelper service = new TestDataWebServiceHelper();
        this.GetData(service);
    }

    private async void SetData(TestDataWebServiceHelper service)
    {
        listA = await this.SetListA(service);
        listB = await this.SetListB(service);
    }

    private async Task<IEnumerable<SelectListItem>> SetListA(TestDataWebServiceHelper service)
    {
        List<String> rawList = new List<String>();
        rawList = await service.GetValuesAsync("json");
        return rawList.Select(x => new SelectListItem { Text = x, Value = x });
    }

    private async Task<IEnumerable<SelectListItem>> SetListB(TestDataWebServiceHelper service)
    {
        List<String> rawList = new List<String>();
        rawList = await service.GetValuesAsync("json");
        return rawList.Select(x => new SelectListItem { Text = x, Value = x });
    }
}

When I call this controller action I receive following error:

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" %>.

So, questions:

  1. Should I somehow mark controller or action or page itself as asynchronous to allow this model initialization?
  2. Is it possible to encapsulate all initialization logic to viewmodel and not to pop it to the controller?
  3. What is the reason of that error? Seem like it related to WebForms, but I use Razor engine.

Upvotes: 1

Views: 4470

Answers (2)

Keval Langalia
Keval Langalia

Reputation: 1892

There a few things to take care about "async" are as follows :

  • The method should only be "async" when it needs to wait for the action to be performed by the code written in that.
  • if there is nothing to be "await" in the method, than that method should not declared as "async".
  • if there is any event (or else event handler) to be marked as async then return type should be "void" or any specific return type
  • but if any other (general) method should marked as "async" then the return type should be Task or Task< return-type > .

Now coming to your questions,

  • definitely you can mark "Controller", "Event" & "Page" as async (I'm not damn sure about marking the page as async coz i never used it) and by that those method will take rest until the action will be performed completely written in that method.
  • And that's the actual system to encapsulate whole the initialization logic in a viewModel.
    • for that make a Floder, named it as "ViewModels" and put all of the viewModel Codes in that folder and whenever needed, use that.

You should put your this code in a ViewModel:

private async void SetData(TestDataWebServiceHelper service)
{
    listA = await this.SetListA(service);
    listB = await this.SetListB(service);
}

private async Task<IEnumerable<SelectListItem>> SetListA(TestDataWebServiceHelper service)
{
    List<String> rawList = new List<String>();
    rawList = await service.GetValuesAsync("json");
    return rawList.Select(x => new SelectListItem { Text = x, Value = x });
}

private async Task<IEnumerable<SelectListItem>> SetListB(TestDataWebServiceHelper service)
{
    List<String> rawList = new List<String>();
    rawList = await service.GetValuesAsync("json");
    return rawList.Select(x => new SelectListItem { Text = x, Value = x });
}

and then you should call it whenever needed.

(Hope this helps...)

Upvotes: 0

svick
svick

Reputation: 244777

There are two problems in your code:

  1. You shouldn't start async void operations from constructor like this. In fact, you usually shouldn't start any async operations from constructor and you also shouldn't use async void methods at all (except for event handlers).

    I think that in your case, async factory method instead of an constructor makes the most sense:

    private GetStartedModel()
    {}
    
    public static async Task<GetStartedModel> Create()
    {
        var service = new TestDataWebServiceHelper();
        var result = new GetStartedModel();
        listA = await result.SetListA(service);
        listB = await result.SetListB(service);
        return result;
    }
    

    For more details, see Stephen Cleary's post on async constructors.

  2. You need to make your controller action async too:

    public async Task<ActionResult> Index()
    {
        var gsModel = await GetStartedModel.Create()
    
        return View(gsModel);
    }
    

Upvotes: 5

Related Questions