Reputation: 1369
I'm having a hard time figuring out how to render a display template for an interface type viewmodel. Assume that I have a viewmodel like the one below:
public class DashboardViewModel
{
public ISelectedWalletsViewModel Wallets { get; set; }
}
public interface ISelectedWalletsViewModel
{
}
public class OneSelectedWalletViewModel : ISelectedWalletsViewModel
{
public SelectedWalletViewModel SelectedWallet { get; set; }
public IEnumerable<WalletViewModel> OtherWallets { get; set; }
}
public class AllSelectedWalletsViewModel : ISelectedWalletsViewModel
{
public IEnumerable<WalletViewModel> Wallets { get; set; }
}
public interface IWalletViewModel
{
long Id { get; set; }
string Name { get; set; }
string Description { get; set; }
string CurrentBalance { get; set; }
}
public class WalletViewModel : IWalletViewModel
{
public long Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string CurrentBalance { get; set; }
}
public class SelectedWalletViewModel : IWalletViewModel
{
public long Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string CurrentBalance { get; set; }
}
So I created the DisplayTemplates folder to set up the corresponding .cshtml files for each and every model:
<!-- START OneSelectedWalletViewModel.cshtml -->
@using CLSoft.MyWallet.Models.Home
@model OneSelectedWalletViewModel
@Html.DisplayFor(m => m.SelectedWallet)
@Html.DisplayFor(m => m.OtherWallets)
<a asp-action="Index">Select all wallets</a>
<!-- END OneSelectedWalletViewModel.cshtml -->
<!-- START AllSelectedWalletsViewModel.cshtml -->
@using CLSoft.MyWallet.Models.Home
@model AllSelectedWalletsViewModel
@Html.DisplayFor(m => m.Wallets)
<!-- END AllSelectedWalletsViewModel.cshtml -->
<!-- START SelectedWalletViewModel.cshtml -->
@using CLSoft.MyWallet.Models.Home
@model SelectedWalletViewModel
<div class="card border-primary">
<div class="card-body">
<h5 class="card-title">@Model.Name</h5>
<h6 class="card-subtitle mb-2 text-muted">@Model.CurrentBalance</h6>
<p class="card-text">@Model.Description</p>
</div>
</div>
<!-- END SelectedWalletViewModel.cshtml -->
<!-- START WalletViewModel.cshtml -->
@using CLSoft.MyWallet.Models.Home
@model WalletViewModel
<div class="card">
<div class="card-body">
<h5 class="card-title">@Model.Name</h5>
<h6 class="card-subtitle mb-2 text-muted">@Model.CurrentBalance</h6>
<p class="card-text">@Model.Description</p>
<a asp-action="Index" asp-route-wallet-id="@Model.Id">select wallet</a>
</div>
</div>
<!-- END WalletViewModel.cshtml -->
Now I'm missing just the controller action method:
[HttpGet]
public async Task<IActionResult> Index(long? walletId)
{
var viewModel = await _service.GetDashboardViewModelAsync(walletId);
return View(viewModel);
}
and the Index.cshtml view code:
@using CLSoft.MyWallet.Models.Home
@model DashboardViewModel
@{
ViewData["Title"] = "Index";
}
<div class="row">
<div class="col">
<h4>Your wallets</h4>
@Html.DisplayFor(m => m.Wallets)
</div>
</div>
And now I should be all set up. The thing is no markup is rendered. During my tests I tried to add a display template for the interface ISelectedWalletsViewModel
@model ISelectedWalletsViewModel
@Html.DisplayForModel()
put a breakpoint on @Html.DisplayForModel() and - ta dah! - the debugger stopped as it should. But, alas, no concrete DisplayTemplate was invoked. Am I missing something?
EDIT 1: I created a github repo to show the problem.
EDIT 2: I also reported the issue on AspNet/Mvc repository
Upvotes: 1
Views: 1925
Reputation: 571
We hit the same issue when migrating to ASP.NET Core, and ended up with the following helper method as a workaround:
public static IHtmlContent MagicDisplayFor<TModel, TResult>(
this IHtmlHelper<TModel> helper,
Expression<Func<TModel, TResult>> expression)
{
// run the expression to get the actual object we want to render
var targetObject = expression.Compile()(helper.ViewData.Model);
// get the runtime type of the object (as opposed to the declared type TResult)
var actualTargetType = targetObject.GetType();
// render the object as usual, but override the template name using the target type
return helper.DisplayFor(expression, templateName: actualTargetType.Name);
}
We then replaced @Html.DisplayFor
with @Html.MagicDisplayFor
in the one place which needed this behavior.
It adds a bit of overhead compared to just using @Html.DisplayFor
directly, and there's probably some edge cases we haven't considered, but it seems to work well enough for us.
Upvotes: 1
Reputation: 1369
Turns out this is by design. I'll quote the answer by NTaylorMullen below, in case the link would die (unlikely, but still):
[...] So in regards to this issue it's actually by design. When moving to Core we found that in legacy ASP.NET we had a bad habit of oversharing (using view model templates that were unintended). To combat this we removed that capability and required users to be explicit in the types that they use when working with display templates. Sorry I don't have a better answer for you!
So, how should you refactor your code to meet these rules?
Upvotes: 1