Binding to a ExpandoObject in Blazor - asp.net-core

I've build a class which has a dynamic list of properties (such as color, size, weight etc etc), the contents of this aren't known at compile time so I used a ExpandoObject
Where I'm struggling is to get Blazor to bind to the value properties of the object for example
#foreach (var option in (IDictionary<string, object>)selectedProduct.Properties)
{
<div class="col-sm">
<input type="text" placeholder="#option.Key" #bind="option.Value"/>
</div>
}
However when this is compiled the autogen code results in an error :- 'KeyValuePair<string, object>.Value' cannot be assigned to
Has anyone had any success binding to a ExpandoObject (and children properties?)

This is not directly related to the ExpandoObject.
When you iterate over a Dictionary you get back immutable KeyValuePair items.
You get an error because #bind="option.Value" compiles to code that wants to assign to option.Value.
And as tuples and anonymous types are all immutable I think you will have to write a <Label, Value> class for this.

Thanks to Henk's answer I was able to modify my code to support the ExpandoObject
#foreach (var option in (IDictionary<string, object>)selectedProduct.Properties)
{
<div class="col-sm">
<input type="text" placeholder="#option.Key" #onchange="#((ChangeEventArgs __e) => ((IDictionary<string, object>)selectedProduct.Properties)[option.Key] = (object)__e.Value.ToString())"/>
</div>
}
Its uglier to look at but does the job

Related

ASP.NET Core Razor Page: Passing value from Foreach to a JavaScript function

I'm trying to pass the value from a C# object to a JavaScript function.
But I am getting an error:
#foreach (var Item in Model.Student)
{
<input id="btnAdd" type="button" value="Add" onclick="AddTextBox(JSON.parse(#Html.Raw(Item)))"/>
}
The Model.Student property returns a Student object, which is defined as follows:
public class Student
{
public int Id { get; set; }
public string name { get; set; }
}
You need to do three things to resolve this issue:
Serialize your Student object (Item) to JSON using Json.Serialize(),
Encode your JSON as a JavaScript string using HttpUtility.JavaScriptStringEncode(), and
Wrap your JSON in apostrophes so that JSON.parse() is correctly interpreting it as a string.
Your final Razor markup should look like the following:
<input id="btnAdd" type="button" value="Add" onclick="AddTextBox(JSON.parse('#Html.Raw(HttpUtility.JavaScriptStringEncode(Json.Serialize(Item).ToString()))'))"/>
Continue reading for a deeper understanding of how to troubleshoot these types of issues in the future, as well as why each one of these steps is necessary.
Debugging
Before diving in, it's worth taking a step back to revisit some troubleshooting principles. Remember that ASP.NET Core MVC is simply rendering an HTML page. After that, it's up to the browser to parse and execute both the HTML and the JavaScript. When you're combining Razor, HTML, and JavaScript in the same line, but only looking at the errors generated by JavaScript, it's easy to miss the underlying issue. Given this, what you want to do is view the source code in your browser to see what HTML is being generated.
Issue #1: Serializing Classes
The most immediate issue is that when you call #Html.Raw(Item), the Item's ToString() method is going to be called to create a string representation of your Student object. If your Student object is a class, this will just be the fully-qualified name of the class (e.g., MyNamespace.Student).
This will output something like:
<input id="btnAdd" type="button" value="Add" onclick="AddTextBox(JSON.parse(MyNamespace.Student))"/>
That's obviously not valid JSON, nor does it include any of your data. To resolve this, you need to wrap the call to Item in Json.Serialize(), which will serialize it from a C# object to a JSON string:
<input id="btnAdd" type="button" value="Add" onclick="AddTextBox(JSON.parse(#Html.Raw(Json.Serialize(Item))))"/>
This will output something closer to what you want:
<input id="btnAdd" type="button" value="Add" onclick="AddTextBox(JSON.parse({"Id"=1,"name":"John"}))"/>
Issue #2: Encoding JSON
If you attempt to execute the above code, you're going to encounter a JavaScript error, such as the following:
Uncaught SyntaxError: Unexpected end of input
Do you see the problem? The quote around Id is going to close your onclick handler, resulting in malformed markup. From the browser's perspective, it ends up seeing the following attributes, trailed by invalid markup:
id: btnAdd
type: button
value: Add
onclick: AddTextBox(JSON.parse('{
To remedy this, you need to encode the JSON string so that it can be embedded within a JavaScript call. Fortunately, ASP.NET Core supplies us with the HttpUtility.JavaScriptStringEncode() method that does just that:
<input id="btnAdd" type="button" value="Add" onclick="AddTextBox(JSON.parse(#Html.Raw(HttpUtility.JavaScriptStringEncode(Json.Serialize(Item).ToString()))))"/>
Note: The JavaScriptStringEncode() method requires a string, so we need to call ToString() on the Json.Serialize() call, which otherwise outputs an IHtmlContent object.
This will now output something like the following:
<input id="btnAdd" type="button" value="Add" onclick="AddTextBox(JSON.parse({\"Id\"=1,\"name\":\"John\"}))"/>
Issue #3: Converting to a JSON object
There are still problems here. The JSON.parse() method expects a string, but we're passing it raw JSON notation. As such, if you execute this code, you'll now receive a variation on the original error:
Uncaught SyntaxError: Invalid or unexpected token
This can be resolved by simply wrapping the JSON in apostrophes, denoting that it's a string:
<input id="btnAdd" type="button" value="Add" onclick="AddTextBox(JSON.parse('#Html.Raw(HttpUtility.JavaScriptStringEncode(Json.Serialize(Item).ToString()))'))"/>
This will now output something like the following:
<input id="btnAdd" type="button" value="Add" onclick="AddTextBox(JSON.parse('{\"Id\"=1,\"name\":\"John\"}'))"/>
If you click on this element, this should finally work—assuming, of course, your AddTextBox() is functioning correctly.
Conclusion
As mentioned at the top, when working with Razor, HTML, and JavaScript, you really need to pay close attention to what HTML is being rendered from the Razor page, prior to it being parsed by the browser or executed by the JavaScript engine. In this case, most of these issues stem from malformed markup that either the browser or the JavaScript engine aren't able to properly parse.

NetCore InputTagHelper using reflection

I have a model with lots of properties. Creating or updating theirs views is a troublesome work.
So i trying to use Reflection to create view during runtime:
(PS: yes, i know the front-end is very import, i should use Vue or other frame to create views at designtime. But I just want to try buildering forms at runtime, yes i am a freak. LOL)
There is my code:
#{
var dic = Model.GetAttributePropsByCategory();// get PropertyInfo by CategoryAttribute and custom DisplayIndexAttribute
}
#foreach (var items in dic)
{
var category = items.Key; // InfoCategoryAttribute
foreach (var tuple in items.Value)
{
var displayIndex = tuple.Item1; // Custom: DisplayIndexAttribute
var prop = tuple.Item2; // PropertyInfo
<input asp-for="#prop.Name" class="form-control"/>
}
}
My target is:
<input class="form-control valid" type="text" data-val="true" data-val-required="The VIN field is required." id="VIN" name="VIN" value="VIN001" aria-describedby="VIN-error" aria-invalid="false">
But the result like this:
<input class="form-control valid" type="text" data-val="true" data-val-required="The Name field is required." id="prop_Name" name="prop.Name" aria-describedby="prop_Name-error" aria-invalid="false">
So, I read the source code in the aspnetcore.mvc.taghelpers. I found the key is InputTagHelper.For:
public class InputTagHelper : TagHelper
{
...
[HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; }
...
}
But I can't understand how the InputTagHelper.For was created. Therefor, i dont know how to override it to achieve my target.
Is there any Suggestions? Thx.
In source code of InputTagHelper, we can find following code snippet will help generate textbox input and the third parameter For.Name would be used to set value(s) of textbox id and name attribute.
return Generator.GenerateTextBox(
ViewContext,
modelExplorer,
For.Name,
modelExplorer.Model,
format,
htmlAttributes);
And if we debug the source code, we will find the value of For look like below.
To achieve your expected result, you can customize it and pass For.Model.ToString() to GenerateTextBox method rather than For.Name, like below.
return Generator.GenerateTextBox(
ViewContext,
modelExplorer,
//For.Name,
For.Model.ToString(),
modelExplorer.Model,
format,
htmlAttributes);
Test Result

SelectList dropdown list showing multiple = "multiple" for current viewmodel

This kind of a bizarre issue and I can't figure out a solution how I want.
I'm using .net core 2.1. I have a orders view model like this:
public class OrdersFilterViewModel
{
[Display(Name = "Account Numbers:")]
public IEnumerable<SelectListItem> AccountNumbers { get; set; }
}
My viewmodel and SelectList in my orders controller is called like this:
var vm = new OrdersFilterViewModel
{
AccountNumbers = new SelectList(_context.Account.Where(m => m.UserID == userId), "AccountNumber", "AccountNumber", account)
};
return PartialView("_FilterOrders", vm);
The problem lies when trying to get a dropdown list in the view which looks like this:
<form asp-action="FilterOrders" asp-controller="Order" id="ordersFilterForm" method="post">
<div class="form-group">
<label asp-for="AccountNumbers" class="control-label"></label>
<select asp-for="AccountNumbers" class="form-control" asp-items="#Model.AccountNumbers">
</select>
</div>
<div class="form-group">
<input type="submit" value="Submit" class="btn btn-default" />
</div>
</form>
This somewhat works but gives me a textarea type display where multiple = "multiple" is always tacked on in the browser. I've discovered that if I add something like the following to my viewmodel:
public int? AccountId { get; set; }
Then change my view to:
<select asp-for="AccountId" class="form-control" asp-items="#Model.AccountNumbers">
I can then have my dropdown list. However, I don't need that property for anything as far as I know. I tried a million things so it's possible I made some other slight changes I'm forgetting to get that to work, but that's the gist of it.
Is there any way around adding that extra property? Or do I need it for something I'm not aware of? Or is there any way to set multiple = "false" or something to that effect so I can get my dropdown list with my original viewmodel and such?
I haven't dealt with the post back to the controller yet, so maybe that will reveal the gotchas. I'm basically trying to create a modal type query filter that doesn't really do much other than modify some parameters and send them back to my query to update it. Thanks.
Is there any way around adding that extra property? Or do I need it
for something I'm not aware of?
Yes, you need this extra property, because in your select there are many items, and the user will select one or multiple items, and on the server side you'll need to know what the user selected, this is the purpose of the select tag.
And the multiple = "multiple" depends on what you put in the asp-for in the case of asp-for="AccountId" it is a single int value, so it won't use multiple, is you have an array in the asp-for then it will use the multiple.
Here is a pretty detailed description about the select tag helper:
Select Tag Helper in ASP.NET Core MVC

Two-Way binding in Windows 8 HTML / JavaScript Metro Apps

I'm trying to achieve a two way binding between an input field and a field in my JavaScript ViewModel. The binding has been wired up declarativly. Unfortunately the changes I do in the UI aren't reflected in my ViewModel.
My Code looks like that (written out of my head as I don't have the code here)
View:
<form data-win-bind="onsubmit: onCalculate">
<div class="field">
Product Name:
<input type ="number" data-win-bind="text:value1"/>
</div>
<div class="field">
Product Price:
<input type ="number" data-win-bind="text:value2"/>
</div>
<div class="field">
Result
<br />
<span data-win-bind="innerText: result" />
</div>
</form>
JavaScript
var model= WinJS.Class.define(
function() {
this.onCalculate = calculate.bind(this);
this.value1 = 0;
this.value2 = 0;
this.result = 0;
},{
value1: 0,
value2: 0,
result: 0
calculate: function() {
this.result = this.value1 + this.value2;
return false;
}
}, {});
// Make the model Observable
var viewModel = WinJS.Binding.as(new model());
WinJS.Binding.processAll(null, viewModel);
When I apply the binding, the ui shows my initial values. The form submition is correctly wired with the calculate function. The values of value1 and value2 however aren't updated with the users input.
What I'm trying to achive is to keep my JavaScript unaware of the underlying view. So I don't want to wire up change events for the html input fields in JavaScript.
Is there any way to achive this with pure WinJS? All samples I've found so far only do a one-way binding and use event listeners to update the ViewModel with changes from the UI.
WinJS only supports one-way binding for Win8. It is necessary to wire up listeners for change events in the UI elements, hence the nature of the samples you've seen. In other words, the implementation of WinJS.Binding's declarative processing doesn't define nor handle any kind of two-way syntax.
It would be possible, however, to extend WinJS yourself to provide such support. Since WinJS.Binding is just a namespace, you can add your own methods to it using WinJS.Namespace.define (repeated calls to this are additive). You could add a function like processAll which would also look for another data-* attribute of your own that specified the UI element and the applicable change events/properties. In processing of that you would wire up a generic event handler to do the binding. Since you have the WinJS sources (look under "References" in Visual Studio), you can see how WinJS.Binding.processAll is implemented as a model.
Then, of course, you'd have a great piece of code to share :)
This article provides a nice solution:
http://www.expressionblend.com/articles/2012/11/04/two-way-binding-in-winjs/

Castle NVelocity Including Variables not Variable contents

I'm using Castles' NVelocity Engine to do some template work. Here's the problem. Several of my templates work fine, but one of them isn't.
#foreach($i in $Items)
<div class="grid_3 folioItem"> <a rel="prettyPhoto[portfolio]" href="$i.Link" class="lightBox"><img src="$i.Image" width="220" height="125" alt="showcase" /></a>
<h4>$i.ShortName</h4>
<p>$i.LongName</p>
<p><a class="button pngFix" href="$i.Link">$i.LinkText</a></p>
</div>
#end
For some reason, the above code works half way. I get six sets of the div tags with all the innards, but Velocity outputs $i.ShortName instead of the contents on $i.ShortName. Any clue why this is? If I get six outputs that would leave me to believe that Items is set up correctly and exists in the Velocity Template. But for some odd reason it's children don't.
Now Items is a List<CategoryItem> and I've checked over and over again to make sure that I haven't misspelled the names of the members.
What am I missing?
Okay. So I figured it out (I think) it seems to be that sub objects will only expose their properties to the template. For instance:
public class Item{
public string BadName;
public stirng GoodName {
get {
return "Foo"
}
}
}
GoodName can be referenced in the template, But BadName cannot