How can I pass Dictionary<string, dynamic> to a blazor component? - dynamic

I am trying to pass a dictionary as a parameter to a blazor component. The dictionary needs to store <string, dynamic>, <string, List>, and <string, Dictionary<string, dynamic>> key-value pairs.
I tried to do this, but get the error, "'EventCallbackFactory' has no applicable method named 'CreateBinder' but appears to have an extension method by that name. Extension methods cannot be dynamically dispatched."
Is what I am trying to do valid, and if not, why? Is there another way I should approach this?
Here is the code for my blazor component, for reference:
#page "/dictitemcomponent"
#using System.Collections.Generic;
<ul>
#foreach (KeyValuePair<string, dynamic> item in thisDict)
{
#if(item.Value.GetType() == typeof(Dictionary<string, dynamic>))
{
<li>#item.Key.ToString() : </li>
#foreach (var dict in item.Value)
{
<DictItemComponent thisDict=dict/>
}
}
#if(item.Value.GetType() == typeof(List<dynamic>))
{
<li>#item.Key.ToString() : </li>
#foreach (var value in item.Value)
{
<li>#value</li>
}
}
#if(item.Value.GetType() != typeof(List<dynamic>) && item.Value.GetType() != typeof(Dictionary<dynamic, dynamic>))
{
<li>#item.Key.ToString() : <input #bind="item.Value"/></li>
}
}
</ul>
#code
{
public KeyValuePair<string, dynamic> newProperty = new KeyValuePair<string, dynamic>();
[Parameter] public Dictionary<string,dynamic> thisDict {get; set;}= new Dictionary<string, dynamic>();
//convert the value of a KVP to a dictionary
public void ValueToProperty(KeyValuePair<string,dynamic> property)
{
string key = property.Key;
property = new KeyValuePair<string, dynamic>(key, new Dictionary<string, dynamic>());
}
public void ValueToList(KeyValuePair<string,dynamic> property)
{
string key = property.Key;
property = new KeyValuePair<string, dynamic>(key, new List<dynamic>());
}
}

May I know what you are trying to achieve?
Based on your first if statement, why would you have a dictionary inside a dictionary?
By reading your exception and your 3rd conditional statement, it seems that you are trying to bind the input to your dictionary value. However, this is not how you should use a dictionary. Unless you are binding to the usual "type" property (e.g. string), you may use #bind=someVariable. Otherwise, in a dictionary, each value should tie to their respective key and therefore, #bind=_dict[key] ("shorthand" syntax for #bind-value and #bind-value:event) instead of binding input to the item.value. For easier understanding, I've scoped out the aforementioned conditional statement and simulated a solution to the problem in the following lines. It should render value in the <label> next to the input during onchange:
#page "/dict"
#foreach (var kvp in _dict)
{
<div>
<label>#kvp.Key</label>
<input #bind=_dict[kvp.Key] />
<label>Key:#kvp.Key|Value:#kvp.Value</label>
}
#code {
private Dictionary<string, string> _dict = new()
{
["1"] = "One",
["2"] = "Two",
["3"] = "Three",
["4"] = "Four",
["5"] = "Five",
};
}
Meanwhile, you should simplify your if statements as follow:
#if(item.Value.GetType() == typeof(Dictionary<string, dynamic>))
{
//dowork
}
else if(item.Value.GetType() == typeof(List<dynamic>))
{
//do more work
}
else
{
//do other work
}
Screenshot for my Input fields rendered based on Key Value Pair and value entered

Related

How to set new value to existing query string Blazor

I'm attempting to change a query string paramater without reloading the page. I have a DateTime Date value that I'm attempting to turn into a string and put into the url. Say, for the sake of this question that it is:
DateTime? Date = new DateTime(2020, 9, 4);
So, my url could begin looking like:
https://localhost:44346/Events?d=2020-10-18
and after I do whatever magic must be done, it ends up like:
https://localhost:44346/Events?d=2020-10-04
I have attempted using NavigationManager and QueryHelpers like the following, but I've had no luck:
QueryHelpers.AddQueryString(navManager.Uri, "d", Date?.ToString("yyyy-MM-dd"));
Please check out this demo
#page "/counter"
#using Microsoft.AspNetCore.WebUtilities
#using System.Web
#inject NavigationManager navManager
<button class="btn btn-primary" #onclick="ChangeUrl">Change url</button>
<br />
demonstrate lack of page reload
<br />
<button class="btn btn-primary" #onclick="AddItem">Add item</button>
#foreach (var item in list)
{
#item
}
#code {
private List<string> list = new List<string>()
{
"Test string"
};
private void AddItem()
{
var uri = navManager.ToAbsoluteUri(navManager.Uri);
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("d", out var param))
{
list.Add(param.First());
}
else
{
list.Add("d is empty");
}
}
DateTime dateCounter = DateTime.Today;
private void ChangeUrl()
{
dateCounter = dateCounter.AddDays(1);
string url = RemoveQueryStringByKey(navManager.Uri, "d");
var query = new Dictionary<string, string> { { "d", dateCounter.ToString("dd-MM-yyyy") } };
navManager.NavigateTo(QueryHelpers.AddQueryString(url, query));
}
public static string RemoveQueryStringByKey(string url, string key)
{
var uri = new Uri(url);
// this gets all the query string key value pairs as a collection
var newQueryString = HttpUtility.ParseQueryString(uri.Query);
// this removes the key if exists
newQueryString.Remove(key);
// this gets the page path from root without QueryString
string pagePathWithoutQueryString = uri.GetLeftPart(UriPartial.Path);
return newQueryString.Count > 0
? String.Format("{0}?{1}", pagePathWithoutQueryString, newQueryString)
: pagePathWithoutQueryString;
}
}

Method that expands into tag-helper

I have the following method in a cshtml file. It simply expands into two label elements. The first is a plain label element. The second however, uses a tag helper:
async Task field(string str)
{
<label for="#str">#str</label>
<label asp-for="#str">#str</label>
}
Here's how I have it defined in the cshtml file along with calling it once:
#{
{
async Task field(string str)
{
<label for="#str">#str</label>
<label asp-for="#str">#str</label>
}
await field("abc");
}
}
If I 'view source' on the result, I see the following:
<label for="abc">abc</label>
<label for="str">abc</label>
Note that the #str argument was properly passed and used in the first case but was not in the second case. So it seems that there's an issue in passing the argument to the tag-helper variant here.
Any suggestions on how to resolve this?
In my opinion, the argument has been passed the tag-helper variant successfully. But the the label asp-for attribute will be rendered as the for attribute with asp-for ModelExpression's name value(str) not the value ModelExpression's model(abc).
According to the label taghelper source codes, you could find the tag helper will call the Generator.GenerateLabel method to generate the label tag html content.
The Generator.GenerateLabel has five parameters, the third parameter expression is used to generate the label's for attribute.
var tagBuilder = Generator.GenerateLabel(
ViewContext,
For.ModelExplorer,
For.Name,
labelText: null,
htmlAttributes: null);
If you want to show the str value for the for attribute, you should create a custom lable labeltaghelper.
More details, you could refer to below codes:
[HtmlTargetElement("label", Attributes = "asp-for")]
public class ExtendedAspForTagHelper:LabelTagHelper
{
public ExtendedAspForTagHelper(IHtmlGenerator generator)
: base(generator)
{
}
public override int Order => -10000;
//public override void Process(TagHelperContext context, TagHelperOutput output)
//{
// base.Process(context, output);
// if (!output.Attributes.TryGetAttribute("maxlength", out TagHelperAttribute maxLengthAttribute))
// {
// return;
// }
// var description = $"Only <b>{maxLengthAttribute.Value}</b> characters allowed!";
// output.PostElement.AppendHtml(description);
//}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
var tagBuilder = Generator.GenerateLabel(
ViewContext,
For.ModelExplorer,
For.Model.ToString(),
labelText: null,
htmlAttributes: null);
if (tagBuilder != null)
{
output.MergeAttributes(tagBuilder);
// Do not update the content if another tag helper targeting this element has already done so.
if (!output.IsContentModified)
{
// We check for whitespace to detect scenarios such as:
// <label for="Name">
// </label>
var childContent = await output.GetChildContentAsync();
if (childContent.IsEmptyOrWhiteSpace)
{
// Provide default label text (if any) since there was nothing useful in the Razor source.
if (tagBuilder.HasInnerHtml)
{
output.Content.SetHtmlContent(tagBuilder.InnerHtml);
}
}
else
{
output.Content.SetHtmlContent(childContent);
}
}
}
}
}
Improt this taghelper in _ViewImports.cshtml
#addTagHelper *,[yournamespace]
Result:

Rendering #Html.Action("actionName","controllerName") at runtime , fetching from database in MVC4

My requirement is to fetch html data from database and render it on view. But if that string contains #Html.Action("actionName","controllerName"), i need to call perticular controller action method also.
I am rendering my html on view using #Html.Raw().
Eg: Below is the html string stored in my database
'<h2> Welcome To Page </h2> <br/> #Html.Action("actionName", "controllerName")'
So when it render the string, it execute mentioned controller and action too.
Any help will be appreciated.
You can try RazorEngine to allow string template in razor executed.
For example, sample code from the project site http://antaris.github.io/RazorEngine/:
using RazorEngine;
using RazorEngine.Templating; // For extension methods.
string template = "Hello #Model.Name, welcome to RazorEngine!";
var result =
Engine.Razor.RunCompile(template, "templateKey", null, new { Name = "World" });
But there is one catch, Html and Url helpers are defined in the Mvc framework, hence it is not supported by default.
I will suggest you try to create your template by passing model so that you don't have to use #Html.Action.
If you can not avoid it, then there is possible a solution suggested by another so answer https://stackoverflow.com/a/19434112/2564920:
[RequireNamespaces("System.Web.Mvc.Html")]
public class HtmlTemplateBase<T>:TemplateBase<T>, IViewDataContainer
{
private HtmlHelper<T> helper = null;
private ViewDataDictionary viewdata = null;
public HtmlHelper<T> Html
{
get
{
if (helper == null)
{
var writer = this.CurrentWriter; //TemplateBase.CurrentWriter
var context = new ViewContext() { RequestContext = HttpContext.Current.Request.RequestContext, Writer = writer, ViewData = this.ViewData };
helper = new HtmlHelper<T>(vcontext, this);
}
return helper;
}
}
public ViewDataDictionary ViewData
{
get
{
if (viewdata == null)
{
viewdata = new ViewDataDictionary();
viewdata.TemplateInfo = new TemplateInfo() { HtmlFieldPrefix = string.Empty };
if (this.Model != null)
{
viewdata.Model = Model;
}
}
return viewdata;
}
set
{
viewdata = value;
}
}
public override void WriteTo(TextWriter writer, object value)
{
if (writer == null)
throw new ArgumentNullException("writer");
if (value == null) return;
//try to cast to RazorEngine IEncodedString
var encodedString = value as IEncodedString;
if (encodedString != null)
{
writer.Write(encodedString);
}
else
{
//try to cast to IHtmlString (Could be returned by Mvc Html helper methods)
var htmlString = value as IHtmlString;
if (htmlString != null) writer.Write(htmlString.ToHtmlString());
else
{
//default implementation is to convert to RazorEngine encoded string
encodedString = TemplateService.EncodedStringFactory.CreateEncodedString(value);
writer.Write(encodedString);
}
}
}
}
Then you have to use HtmlTemplateBase (modified base on https://antaris.github.io/RazorEngine/TemplateBasics.html#Extending-the-template-Syntax):
var config = new TemplateServiceConfiguration();
// You can use the #inherits directive instead (this is the fallback if no #inherits is found).
config.BaseTemplateType = typeof(HtmlTemplateBase<>);
using (var service = RazorEngineService.Create(config))
{
string template = "<h2> Welcome To Page </h2> <br/> #Html.Action(\"actionName\", \"controllerName\")";
string result = service.RunCompile(template, "htmlRawTemplate", null, null);
}
in essence, it is telling the RazorEngine to use a base template where mvc is involved, so that Html and Url helper can be used.

RadioButton list Binding in MVC4

I have a radiobuttonList which is binding data from Enum Class and its working correctly in the view.
But my concern is how can I set inital value of radiobutton to CROCount.ONE.I have tried to set the initial value in the following way but couldnot get the desired result.
public enum CROCount
{
ONE = 1,
TWO = 2
}
ViewModel is
public class RegistraionVM
{
....
public EnumClass.CROCount CROCount { get; set; }
}
I generated the radio button list as follows.
<div>
#foreach (var count in Enum.GetValues(typeof(SMS.Models.EnumClass.CROCount)))
{
<label style="width:75px">
#Html.RadioButtonFor(m => m.RegistrationVenue, (int)count,
new { #class = "minimal single" })
#count.ToString()
</label>
}
</div>
Binding performed in the Controller is
public ActionResult Index(int walkInnId)
{
try
{
var _studentReg = new RegistraionVM
{
CROCount=EnumClass.CROCount.ONE
};
return View(_studentReg);
}
catch (Exception ex)
{
return View("Error");
}
}
Your binding your radio button to property CROCount (not RegistrationVenue) so your code should be
#Html.RadioButtonFor(m => m.CROCount, count, new { id = "", #class = "minimal single" })
Note that the 2nd parameter is count (not (int)count) so that you generate value="ONE" and value="TWO". Note also the new { id = "", removes the id attribute which would otherwise result in duplicate id attributes which is invalid html.

MVC4 ViewData.TemplateInfo.HtmlFieldPrefix generates an extra dot

I'm trying to get name of input correct so a collection of objects on my view model can get bound.
#{ViewData.TemplateInfo.HtmlFieldPrefix = listName;}
#Html.EditorFor(m => m, "DoubleTemplate", new {
Name = listName,
Index = i,
Switcher = (YearOfProgram >= i +1)
})
As you can see here, I pass in the "listName" as the prefix for my template, the value of listName = "MyItems".
And here is my template:
#model Web.Models.ListElement
#if (ViewData["Switcher"] != null)
{
var IsVisible = (bool)ViewData["Switcher"];
var index = (int)ViewData["Index"];
var thisName = (string)ViewData["Name"] + "[" + index + "].Value";
var thisId = (string)ViewData["Name"] + "_" + index + "__Value";
if (IsVisible)
{
#*<input type="text" value="#Model.Value" name="#thisName" id ="#thisId" class="cell#(index + 1)"/>*#
#Html.TextBoxFor(m => m.Value, new { #class ="cell" + (index + 1)})
#Html.ValidationMessageFor(m => m.Value)
}
}
but I found that the generated name becomes this: MyItems.[0].Value
It has one extra dot. How can I get rid of it?
Incidentally, I tried to manually specify the name inside the template and found the name gets overridden by the Html helper.
Update
The reason why I have to manually set the HtmlFieldPrefix is the property name (MyItems which is a list of objects) will get lost when MyItems is passed from main view to the partial view. By the time, the partial view called my template and passed in one object in MyItems, the template itself has no way to figure out the name of MyItems as it has been lost since the last "pass-in".
So that's why I have to manually set the html field prefix name. And I even tried to use something similar to reflection(but not reelection, I forgot the name) to check the name of passed in object and found it returned "Model".
Update 2
I tried Stephen's approach, and cannot find the html helper PartialFor().
I even tried to use this in my main view:
Html.Partial(Model, "_MyPartialView");
In Partial View:
#model MvcApplication1.Models.MyModel
<h2>My Partial View</h2>
#Html.EditorFor(m => m.MyProperty)
Here is my templat:
#model MvcApplication1.Models.ListElement
#Html.TextBoxFor(m => m.Value)
Here is my Model:
public class MyModel
{
private List<ListElement> myProperty;
public List<ListElement> MyProperty
{
get
{
if (myProperty == null)
{
this.myProperty = new List<ListElement>() { new ListElement() { Value = 12 }, new ListElement() { Value = 13 }, new ListElement() { Value = 14 }, new ListElement() { Value = 15 }, };
}
return this.myProperty;
}
set
{
this.myProperty = value;
}
}
}
public class ListElement
{
[Range(0, 999)]
public double Value { get; set; }
}
And here is my controller:
public ActionResult MyAction()
{
return View(new MyModel());
}
It only generates raw text("12131415") for me, instead of the wanted text box filled in with 12 13 14 15.
But if I specified the template name, it then throws an exception saying:
The template view expecting ListElement, and cannot convert
List<ListElement> into ListElement.
There is no need set the HtmlFieldPrefix value. MVC will correctly name the elements if you use an EditorTemplate based on the property type (and without the template name).
Assumed models
public class ListElement
{
public string Value { get; set; }
....
}
public class MyViewModel
{
public IEnumerable<ListElement> MyItems { get; set; }
....
}
Editor template (ListElement.cshtml)
#model YourAssembly.ListElement
#Html.TextBoxFor(m => m.Value)
Main view
#model YourAssembly.MyViewModel
...
#Html.EditorFor(m => m.MyItems) // note do not specify the template name
This will render
<input type="text" name="MyItems[0].Value" ...>
<input type="text" name="MyItems[1].Value" ...>
....
If you want to do this using a partial, you just can pass the whole model to the partial
MyPartial.cshtml
#model #model YourAssembly.MyViewModel
#Html.EditorFor(m => m.MyItems)
and in the main view
#Html.Partial("MyPartial")
or create an extension method
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
string name = ExpressionHelper.GetExpressionText(expression);
object model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
var viewData = new ViewDataDictionary(helper.ViewData)
{
TemplateInfo = new System.Web.Mvc.TemplateInfo { HtmlFieldPrefix = name }
};
return helper.Partial(partialViewName, model, viewData);
}
}
and use as
#Html.PartialFor(m => m.MyItems, "MyPartial")
and in the partial
#model IEnumerable<YourAssembly.ListElement>
#Html.EditorFor(m => m)
Call your partial this way:
#Html.Partial("_SeatTypePrices", Model.SeatTypePrices, new ViewDataDictionary
{
TemplateInfo = new TemplateInfo() {HtmlFieldPrefix = nameof(Model.SeatTypePrices)}
})
Partial view:
#model List
#Html.EditorForModel()
Editor template implementation:
#using Cinema.Web.Helpers
#model Cinema.DataAccess.SectorTypePrice
#Html.TextBoxFor(x => Model.Price)
This way your partial view will contain list of items with prefixes.
And then call EditorForModel() from your EditorTemplates folder.
I found that I can change the value of HtmlFeildPrefix in my template.
So what I did to solve my problem was just to assign the correct value to HtmlFeildPrefix in the template directly rather than in the page which calls the template.
I hope it helps.
If I want to pass the HtmlFieldPrefix I use the following construct:
<div id="_indexmeetpunttoewijzingen">
#Html.EditorFor(model => model.MyItems, new ViewDataDictionary()
{
TemplateInfo = new TemplateInfo()
{
HtmlFieldPrefix = "MyItems"
}
})
</div>