Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

InvalidOperationException rendering ViewComponent in Strongly-Typed View

Recently updated dotnet core 1.0.1 to 1.1 and ViewComponent in MVC starts failing with the below exception:

InvalidOperationException: One or more errors occurred. (The model item passed into the ViewDataDictionary is of type 'App.Models.HomeViewModel', but this ViewDataDictionary instance requires a model item of type 'App.Components.LoginViewComponent'.)

The Index.cshtml renders LoginViewComponent:

@model App.Models.HomeViewModel
<html>
   @Component.InvokeAsync("LoginViewComponent")
</html>

Views/Home/Index.cshtml

The ViewComponent LoginViewComponent/Default.cshtml is just a class that displays it's model's info:

@model App.Components.LoginViewCompoent
<body>
    <div>@Html.DisplayFor(v=>v.LoginName)</div>
</body>

Views/Shared/Components/LoginViewComponent/Default.cshtml

It renders fine when the @model directives is removed from Default.cshtml.

Isn't ViewComponent suppose to be separated and agnostic from the parent view that is wrapping it? From the exception it seems that it would be require to declare the LoginViewComponent ViewComponent in HomeViewModel class in order to render it.

Couldn't find any change note on this on asp.net core github.

Comment and help on this would be greatly appreciated.

like image 617
danial Avatar asked Nov 21 '16 13:11

danial


2 Answers

I've came across the same error, someone in my team updated Microsoft.AspNetCore.Mvc version from 1.0.0 to 1.1.0 and some of the components I had in Strongly-Type views started throwing

InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'My.StronglyView.ObjectType', but this ViewDataDictionary instance requires a model item of type 'My.ViewComponent.ObjectType'.

I am not sure if this change was intentional, but it is definitely not what we would expect.

I haven't got the time to research about the reasons of this 'breaking' change but I came with a solution.

Thing is, if you pass a null object to your ViewComponent.View() method, we get this exception. Any non-null object passed through it, would update the ViewData.ModelExplorer and the correct object-type would have been registered, avoiding this exception.

Using Tester-Doer pattern, same pattern used in some classes of .Net Framework, We can now pass a non-null object to the ViewComponent and use it and its wrapped object as We need.

What I did was, I created a interface IViewComponentModel<T> as class ViewComponentModel<T> as below:

// Interface for ViewComponentModel
public interface IViewComponentModel<T> 
    where T : class
{
    T Data { get; }
    bool HasData();
}

// ViewComponentModel class for the Strongly-Type View Component
public class ViewComponentModel<T> : IViewComponentModel<T>
    where T : class
{
    public T Data { get; private set; }

    public ViewComponentModel(T data)
    {
        Data = data;
    }

    public bool HasData() => Data != null;
}

In my ViewComponent class implementation, I return View(new ViewComponentModel<AnyReferenceType>(myModel));

public async Task<IViewComponentResult> InvokeAsync()
{
    var myViewData = _myService.GetSomeObjectForMyViewComponent();
    var model = new ViewComponentModel<MyViewDataType>(myViewData);

    return await Task.FromResult(View(model));
}

And finally, in my Component View (.cshtml) I have the code below:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model ViewComponentModel<MyViewDataType>

Now you can do whatever you want with this object inside of the view and to use your real model, just call @Model.Data and voilà.

In this way, We will never pass a null object to the ViewComponent and it won't 'inherit' the object type from the View.

Hopefully it helps!

asp.net-core asp.net-core-mvc dotnet-core asp.net-core-1.1

like image 85
Alexz Avatar answered Jan 04 '23 02:01

Alexz


Simple solution is that, in ViewComponent class check null for view model, and if view model is null, then return empty Content result as following:

public async Task<IViewComponentResult> InvokeAsync()
{
    var vm = some query for ViewModel;

    if(vm != null)
    {
        return View(vm);
    }

    return Content("");
}
like image 27
Mohammad Akbari Avatar answered Jan 04 '23 01:01

Mohammad Akbari