DisplayTemplate being ignored (covarient interface?) - c#

This is a weird one. I have the following view file (Views/Search/Submit.cshtml):
#model IEnumerable<KeyValuePair<string, ISearchProvider>>
#foreach (var provider in Model)
{
var results = provider.Value.Results.Take(10);
if (results.Count() > 0)
{
<text><li class="dropdown-header">#provider.Key</li></text>
#Html.DisplayFor(x => results)
}
}
... where results is a System.Collections.Generic.IEnumerable<out T>, and T is ISearchMatch.
I have then defined a display template in Views/Search/DisplayTemplates/SiteSearchMatch.cshtml;
#model SiteSearchMatch
<li>#Html.ActionLink(Model.Name, "details", "site", new { Id = Model.Id }, null)</li>
... and SiteSearchMatch implements ISearchMatch like so;
public class SiteSearchMatch: ISearchMatch
{
public int Id { get; set; }
public string Name { get; set; }
}
I'd expect that my display template gets used; but it doesn't. Instead, the output I see being output is;
<li class="dropdown-header">sites</li>
11147166811481897189813271028
... where that string of numbers is the combination of all the Ids of the ISearchMatch's I wanted to render via the display template.
It seems Razor is simply rendering the ISearchMatch using the first attribute defined in the class; if I remove the definition of the Id property, I instead see the combination of all the Name's of the ISearchMatch's.
Does anyone know why this is happening, and how I can get Razor to use the display template I've specified?

Your expectation is wrong:
I'd expect that my display template gets used; but it doesn't.
The output you see is the ID's simply listed. I suspect your ISearchMatch-interface does only expose the Id-property, but this does not matter. What matters is the actual type of the instance of the result. In your case the following line:
#Html.DisplayFor(x => results)
can be implicitly evaluated as
HtmlHelper<IEnumerable<KeyValuePair<string, ISearchProvider>>>
.DisplayFor<IEnumerable<KeyValuePair<string, ISearchProvider>>, IEnumerable<ISiteMatch>>
(Func<IEnumerable<KeyValuePair<string, ISearchProvider>>, IEnumerable<ISiteMatch>> expression);
Looks pretty complex, but basically it's just a implicit substitution of your model and expression result. Your model is of type IEnumerable<KeyValuePair<string, ISearchProvider>>. That's also the type for the input of your lampda-expression. The result is of type IEnumerable<ISiteMatch>. And here come's the important thing!
The DisplayFor implementation checks, if the result type is enumerable or not. If not, it searches for a fitting template for the type, otherwise it will iterate through the elements and does this for all elements. 1
Searching for a template works based on the type name. In your case the template uses the name of the enumerated type, which is ISearchMatch. It does not find any display template, so it simply dumps the properties, resulting in what you see:
11147166811481897189813271028
To fix this problem, you need to convert your result set to the correct type first. You can do this in different ways. Either you cast the whole result of your provider results:
var results = provider.Value.Results
.Cast<SiteSearchMatch>()
.Take(10);
or you cast them individually within your lamda expression:
#Html.DisplayFor(x => (SiteSearchMatch)results)
The important thing is, that the scalar result type is the same as the model in your display template.
1 Note that this is a little bit more complex, for example the the extension also keeps track of an index and applys it to the output, so that the model could be bound for postback purposes.

The lame answer is that the "Build Action" on my View file Views/Search/DisplayTemplates/SiteSearchMatch.cshtml was set to "None", rather than "Content".
This meant the code worked fine when running in Debug mode within Visual Studio, but didn't work when any deployment was made.
Just to reiterate; this fix required no code changes. Simply change the "Build Action" back to "Content".

Related

Blazor error "The call is ambiguous between the following methods or properties" when working with InputSelect

I am working on a server side Blazor application. My goal is to dynamically populate a dropdown with a list of objects, then do something with that object when it is selected. This is the error I am getting:
The call is ambiguous between the following methods or properties: 'RenderTreeBuilder.AddAttribute(int, string, string?)' and 'RenderTreeBuilder.AddAttribute(int, string, MulticastDelegate?)'
The error is in the file "ListDropdownComponent.g.cs" I have not made any changes to this file, so I suspect Visual Studio is pointing me to the wrong source.
When I comment out this bit of code the error goes away, so I think this is where the error is:
<InputSelect #bind-Value="SelectedList" TValue="ListModel" #onchange="AddToList">
<option value="#null">Add to list</option>
#foreach (ListModel list in Lists)
{
<option value="#list">#list.ListName</option>
}
<option value="#(new ListModel{ ListID = -1, ListName="New List" }) ">New List</option>
</InputSelect>
SelectedList is using the ListModel class:
public ListModel SelectedList;
UPDATE: I switched to using the int ID property of my class and I am still getting the same error. So I have no idea what the cause is.
UPDATE 2: This is the problem: value="#null". I will find a way to work around this, but since int? is nullable I would still like to know what the actual issue is.
This is the problem: value="#null". I will find a way to work around this, but since int? is nullable I would still like to know what the actual issue is
The code you wrote:
<option value="#null" selected>Add to list...</option>
Is transformed to more involved code that builds the page. Your #null there gets transformed in a *.g.cs file that will end up looking something like:
__builder2.AddAttribute(13, "ValueChanged", global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<Microsoft.AspNetCore.Components.EventCallback<int?>>(Microsoft.AspNetCore.Components.EventCallback.Factory.Create<int?>(this, global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.CreateInferredEventCallback(this, __value => _selectedList = __value, _selectedList))));
__builder2.AddAttribute(14, "ValueExpression", global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<System.Linq.Expressions.Expression<System.Func<int?>>>(() => _selectedList));
__builder2.AddAttribute(15, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((__builder3) => {
__builder3.OpenElement(16, "option");
__builder3.AddAttribute(17, "value",
#nullable restore
#line 15 "C:\repos\path\to\your\project\Pages\Whatever.razor"
null
#line default
#line hidden
#nullable disable
);
Your null from the page markup has ended up placed under the #line 15 in the generated code. You can see your page markup is transformed into other code, and that other code builds the page html. The problem has come because your use of null where you did has led to code being generated that calls:
__builder3.AddAttribute(17, "value", null);
But there are two overloads of AddAttribute that match this:
RenderTreeBuilder.AddAttribute(int, string, string?)
RenderTreeBuilder.AddAttribute(int, string, MulticastDelegate?)
The compiler can't know which one to call. It's exactly the same as writing this code:
static void Main(){
Hello(null); //error: the call is ambiguous between Hello(string) and Hello(int?)
}
static void Hello(string x) { }
static void Hello(int? x) { }
If you want to overcome this kind of thing, you can cast the null to a particular type so the compiler can know which overload you want to call:
Hello((string)null);
As such it would be possible to amend your code to be like:
<option value="#(int?)null" selected>Add to list...</option>
And that would make the error go away, because the compiler would know which overload to call..
..but it introduces another problem, and looking at the code as a whole we don't generally write Blazor like this anyway.
Blazor has a data binding focus; you bind page elements to variables and you then use those variables to make other decisions. You don't take the mindset of "have an event that is fired when the combo changes, and in that event handler you get the selected index of the combo and get the..." - that's quite a Windows Forms way of looking at the world. In Blazor it's more like "you bind the combo to the X variable, then when you come to make some decision about what to save in the DB (like when the user presses Save), you look at the value of X"
As such, you might code something more like:
...
<InputSelect #bind-Value="_selectedListId" TValue="int" >
<option value="-1">Choose a list...</option>
#foreach (var list in _existingLists)
{
<option value="#list.ListID">#list.ListName</option>
}
<option value="-2">New List</option>
</InputSelect>
#if(_selectedList == -2){
called:
<InputText #bind-Value="_newListName" ></InputText>
}
...
#code{
class ListModel{ public string ListName { get; set; } public int ListID { get; set; } }
int _selectedListId = -1;
string _newListName;
List<ListModel> _existingLists = new(){
new ListModel { ListID = 100, ListName = "Red Team" },
new ListModel { ListID = 101, ListName = "Blue Team" }
};
}
You bind the Select to the _selectedListId private int field. You give everything in the list Options a number for its value=. Perhaps you're opening in "edit-an-existing-X" mode and your initialize routine looks in some DB and gets the chosen list number for whatever is being shown, and sets _selectedListId to 100 etc. Or maybe this is the "add-new-X" page and we default the selected list id to -1.
The combo is rendered with all its option items; in the code above the _selectedlistId is -1, so the combo is showing at "Choose a list..". The user opens the combo and chooses "Red Team". _selectedListId is set to 100 as a consequence of them doing this - you don't need to act on this right away - they chose Red Team, _selectedListId is now 100, and it will still be 100 when time comes to save it to DB or whatever..
In a similar vein, if they choose "Add to new list..", that sets _selectedListId to -2, and when a re-render occurs, this now means that the if(_selectedListId == -2) in the markup is true, so more things are rendered - a textbox appears for the user to type their new list name in. That textbox's value is #bind'd to _newListName, so when time comes to save, you can read _selectedListId of -2, you can invoke the int CreateNameListNamed(string name) method, which creates the new list and returns the id 102 etc, then you can save your data with 102 as the list ID..
What I'm trying to get to, is that generally we look for ways to leverage this "bind inputs so they change state, make rendering or logic decisions from state" process, rather than that WinFormsy "bind handlers to events on control X, query control X for selection, change properties of control Y as a result" approach
If this doesn't go far enough in terms of what you want to do when the user makes a selection you can, instead of using #bind-Value (which generates a two way binding by created pre-formed behaviors on the Value/ValueExpression/ValueChanged properties) manually specify behaviors for these.
I'm sure plenty of blogs will tell you about #bind-Value vs Value + ValueChanged + ValueExpression but in essence, mostly you can get away with #bind-Value, and you can even make the setting of the property to trigger your additional action if it makes sense:
<InputSelect #bind-Value="SelectedListId" TValue="int" >
int _selectedListId;
int SelectedListId {
get => _selectedListId;
set { _undoHistoryList.Add(_selectedListId); _selectedListId = value; }
}
See how the setting of the property also updated the undo history? This means we carry on using bind-Value, because it is clean and simple and trigger these extra bits from the code in the context of the set
You could also split out and provide your own:
<InputSelect Value="_selectedListId" ValueExpression="() => selectedListId" ValueChanged="x => { _undoHistory.Add(_selectedListId); _selectedListId = x; }" TValue="int" >
..but strive to "do it with #bind-" where you can
For one, html attributes cannot have null values, this is described in the docs here:
HTML attributes can't have null values. The closest equivalent to null
in HTML is absence of the HTML value attribute from the
element.
When selecting an <option> with no value attribute, the browser treats
the value as the text content of that <option>'s element.
Having said that, I don't see why Blazor shouldn't be able to handle this case for you. In fact, there is an issue posted about that here. So, I guess the current answer would be Currently Blazor does not support this use case.
The workaround for this is to use an empty string "" and do the conversion between null yourself.
Your fundamental problem is here:
<option value="#list">#list.ListName</option>
where list is an object. If you check what you produce in the browser Dev tools it will look something like this:
<select>
<option value="BlazorApp4.Pages.Index+ListModel">French</option>
<option value="BlazorApp4.Pages.Index+ListModel">Spanish</option>
<option value="BlazorApp4.Pages.Index+ListModel ">New List</option>
</select>
value in an option must be a string.
You need to rethink how you code this. Without more context I'd be shooting in the dark making suggestions.

What does DisplayNameFor do?

I'm learning .NET Core with Razor pages, using one of the official tutorials here, and I'm having trouble with this code:
#Html.DisplayNameFor(model => model.Movie[0].Title)
The tutorial says:
The DisplayNameExtensions.DisplayNameFor HTML Helper inspects the Title property referenced in the lambda expression to determine the display name. The lambda expression is inspected rather than evaluated. That means there is no access violation when model, model.Movie, or model.Movie[0] is null or empty. When the lambda expression is evaluated, for example, with #Html.DisplayFor(modelItem => item.Title), the model's property values are evaluated.
Which I can't make heads or tails of. What does inspect mean here? Does it mean the lambda function runs in an try/catch, to prevent the access violation errors that the docs speak of? What does it mean exactly?
And in what important way is the second example (with DisplayFor) different? It uses DisplayFor instead of DisplayName, and another change is that it uses ModelItem instead of model. I don't know where it would get ModelItem from, model is made available by #model (...) at the op of the razor page but how ModelItem gets here is not clear to me.
The docs for DisplayNameForare here, but the tutorial links to the non-core docs here, both of which are too terse for me to make much sense of.
DisplayNameFor will look for the Name property of the Display property attribute and print it to your razor page (or the property name itself, if it can't find it). So, if your model is
class Foo
{
[Display(Name="My name")]
public string Prop1 { get; set; }
public string Prop2 { get; set; }
}
It will display My name for Prop1 and Prop2 for Prop2.
The part about it being inspected rather than evaluated, means that it will look at your model definition, it will not try to get a value, so if the model value is null, it will not throw.
On the other hand, DisplayFor will print the value of the selected property, applying any format described by DisplayFormatAttribute, so the model is evaluated and cannot be null.
Typically you will use DisplayNameFor to build the table headers and DisplayFor to build the table data.
What does inspect mean here?
DisplayNameFor is used to get the name of the property and not the value of it, so you will get Title and not Pets 2. In the second case the lambda expression would be evaluated - the first element of the movie list would be retrieved and then its title, that is not happening here.
The difference between evaluating and inspecting is similar to that between reading a book vs turning it around to find out who published it.
2)
And in what important way is the second example (with DisplayFor) different?
DisplayFor is used to format the data itself (Pets 2 and not the Title). In this case there is not much formatting involved because it is a string, but if it was a number for example you could specify how many digits you want to see ect.
3)
I don't know where it would get ModelItem from
ModelItem it is just the name of the variable to pass into the function. modelItem => item.Title is an equivalent of MyFunction(Movie modelItem){return item.Title;} modelItem can be called anything else, particularly as they don't even use it in the function itself and use item directly.

Unable to Pass Anonymous Object as additionalViewData When Using DisplayExtensions.DisplayFor Method

According to Microsoft's documention, I should be able to pass an anonymously typed object as additionalViewData when calling #Html.DisplayFor; however, when I do this, I receive a yellow screen stating:
The model item passed into the dictionary is of type
'System.Collections.Generic.List'1[Surveys2.Models.ReportingSidebarItemViewModel]', but this dictionary requires a model item of type 'Surveys2.Models.ReportingSidebarItemViewModel'.
Here is part of my view:
#model Surveys2.Models.ReportingPageViewModel
#Html.DisplayFor(m => m.Pages, "ReportingSidebarItemViewModel", new { PageType = Model.PageType } )
Here is my controller action:
public ActionResult Summary(string projectCode)
{
ReportingPageViewModel reportingPageViewModel = GetReportingPageViewModel(new ReportingPageParams { ProjectCode = projectCode });
return View("Page", reportingPageViewModel);
}
The problem here is that you're targeting the display page by using the second param of DisplayFor. When you call displayfor(m=> m.prop) without a target view the ViewEngineCollection looks for the best suited diplay page. When the best suited display page is only for a single item and you passed a list it will iterate for you. The targeted display for assumes you are passing the exact type of the page you are targeting and thus it breaks.
EDIT-- Interestingly enough the MSDN Docs don't talk about looping except on the DisplayFor(m=> m.prop) method

ASP.NET MVC Dropdownlist

I have:
public partial class Ingredient
{
public int IngredientID { get; set; }
public string IngredientName { get; set; }
}
in controller:
IEnumerable<Ingredient> ListOfIngridient = FRE.Ingredient.Select(key => key).ToList();
ViewBag.list = new SelectList(ListOfIngridient);
The main problem is that subscribtion of drop down label is wrong
I want to place there 'IngredientName'. How to fix it?
When you use this constructor:
SelectList(ListOfIngridient)
The SelectList doesn't know what is a "text" or "value" field in the supplied collection. So it simply defaults to .ToString() for both. And the default value for .ToString() for an object (unless overridden in your class, which it isn't) is the name of the class.
So both the text and the value for that select in the resulting view are the string representation of each object in the collection.
You can use a different constructor to supply it with the "text" and "value" fields to use:
SelectList(ListOfIngridient, "IngredientID", "IngredientName")
You need to use a different overload when creating your select list. There is one you can use to tell it which properties to use for the names and values.
var ingredients = FRE.Ingredient.ToList();
ViewBag.list = new SelectList(ingredients, nameof(Ingredient.IngredientID),
, nameof(Ingredient.IngredientName));
You should use the nameof operator to avoid magic strings. This has several advantages over manually putting in the strings "IngredientID" and "IngredientName". If you ever use your IDE's refactoring tools to change the names of those properties, they'll automatically be updated. Also, you get Intellisense when typing them, removing the chance of making a typo.
Additionally, grabbing the ingredients can be simplified by removing the Hungarian notation. Hungarian notation goes against Microsoft's C# style guides. Also, you can use the implicit operator var instead of manually declaring what type your ingredients are. Also, there's no need for the select statement. Select statements are used to transform the item in your LINQ query, but you weren't transforming anything.
Lastly, you're passing this information via ViewBag. You should avoid ViewBag if at all possible. It's much better to pass things in a strongly typed model.

ASP.NET MVC not finding Display Template partial view

I'm not sure what I'm doing wrong here. The default display template for a model I'm using is not being used.
This code is in my main action view:
#if (Model.EmbeddedMediaModels != null)
{
foreach (var mediaItem in Model.EmbeddedMediaModels)
{
BitmapFigureModel bitmap = mediaItem as BitmapFigureModel;
if (bitmap != null)
{
var mm = ModelMetadata.FromLambdaExpression(p => bitmap, this.ViewData);
var modelTypeName = mm.ModelType.Name; // = "BitmapFigureModel"
// Neither resolve the template.
// Html.DisplayFor(m => bitmap);
Html.DisplayFor(m => bitmap, modelTypeName);
}
}
}
The Model.EmbeddedMediaModels property is a collection of EmbeddedMediaModel base types, at present it just contains one object, a BitmapFigureModel which derives from EmbeddedMediaModel.
It's tempting to think that this is confusing matters, but the ModelMetadata instance retrieved is quite able to see the correct BitmapFigureModel model type.
Besides, even if I specify the model type name in the call to DisplayFor it still doesn't work.
And here's proof that a correctly-named display template partial view is in place.
What am I doing wrong?
Contrary to the advice from Brad Wilson (ASP.NET team):
The expression-based versions are primarily used for pulling values
from the model (they are parametrized by the current model, as shown
in the example above). They can also be used for pulling values from
some source other than the model or ViewData (for example, with an
expression like “model => someOtherValue” which ignores the model
entirely). This makes them useful in loops.
http://bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-1-introduction.html
It actually seems that its not possible to "ignore the model entirely". In the comments under my question DaveParsons suggests to experiment by just newing-up a model instance and passing it into DisplayFor, this leads to the error:
Templates can be used only with field access, property access,
single-dimension array index, or single-parameter custom indexer
expressions.
So it appears that I should stop being a smarty-pants and just use Html.Partial as Ehsan Sajjad suggests.
do like this:
#Html.DisplayFor(m => mediaItem.Name)
if you want to load the partial view:
#Html.Partial("~/Views/Shared/DisplayTemplates/BitmapFigureModel.cshtml", mediaItem)
or:
#Html.RenderPartial("~/Views/Shared/DisplayTemplates/BitmapFigureModel.cshtml", mediaItem)
In your BitmapFigureModel.cshtml:
#model BitmapFigureModel

Categories

Resources