How can I set TextBox value in MVC5 from ViewBag which contains a list? As you can see my list is in Viewbag.photos and I want to have each value of photo.id in my TextBox and then pass it to controller
#foreach (var photo in ViewBag.photos)
{
#if (#photo.comment != null)
{
<h6>#photo.comment</h6>
}
else
{
<h6> - </h6>
}
#Html.TextBox("photoID", #photo.id)
}
Trying to do that I get an error:
Error CS1973 'HtmlHelper>' has no applicable method
named 'TextBox' but appears to have an extension method by that name.
Extension methods cannot be dinamically dispached.
Maybe there's another workaround?
This is happening because ViewBag.photos is a dynamic object. The compiler cannot know its type, so you have to manually cast it to its original type.
For example:
#Html.TextBox("photoID", (int)photo.id)
As a side note (I'm not sure whether this will prevent your code from working, but it's good practice anyway), you also have bit too many #s: to cite Visual Studio, once inside code, you do not need to prefix constructs like "if" with "#". So your final code will look like:
#foreach (var photo in ViewBag.photos)
{
if (photo.comment != null)
{
<h6>#photo.comment</h6>
}
else
{
<h6> - </h6>
}
#Html.TextBox("photoID", (int)photo.id)
}
You should also consider using ViewModels instead of ViewBag to pass data between your controllers and your views.
Related
In controller I have:
string[] checkedBoxes
ViewBag.Funds = checkedBoxes;
checkedBoxes is posted by a form in the view page. How do I check inside the view if ViewBag.Funds contains a specific string?
I tried:
#if (ViewBag.Funds.ContainsKey("a"))
{
}
I got this error: RuntimeBinderException: 'System.Array' does not contain a definition for 'ContainsKey'
.Contains() also doesn't work even though I used #using System.Linq
You have to cast the ViewBag property to the type first. Try this
#if (((string[])ViewBag.Funds).Contains("a"))
{
}
#if (ViewBag.Funds.Any(s=>s.Contains("a"))))
{
}
I am very new to razor syntax & C# generally, and I am just working through a few blazor courses and got stuck getting my head around the use of a lambda in one of the examples (here for ref https://learn.microsoft.com/en-us/aspnet/core/tutorials/build-your-first-blazor-app?view=aspnetcore-3.1#build-a-todo-list
this bit:
<h3>Todo (#todos.Count(todo => !todo.IsDone))</h3>
The whole code is below... my problem is that I understand that it is evaluating whether members of the todos list are true/false, and then putting the count of these onto the page, and also that todo is a local variable within the Lambda (because if I change it to todoxxx => !todoxxx.IsDone it still works).
What I don't understand is how it is checking each entry in the todos list to evaluate it?
I apologise profusely in advance for what is probably a very simple question!
#page "/todo"
<h3>Todo (#todos.Count(todo => !todo.IsDone))</h3>
<ul>
#foreach (var todo in todos)
{
<li>
<input type="checkbox" #bind="todo.IsDone" />
<input #bind="todo.Title" />
</li>
}
</ul>
<input placeholder="Something todo" #bind="newTodo" />
<button #onclick="AddTodo">Add todo</button>
#code {
private IList<TodoItem> todos = new List<TodoItem>();
private string newTodo;
private void AddTodo()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
}
}
}
If you look at the source code for the IEnumerable.Count, you will see that, behind the scenes, it does exactly that:
Runs a foreach loop on the collection;
Tests your lambda code against every collection member;
If lambda results in true then the counter variable is incremented;
In the end, it returns the counter value.
In this particular context, your lambda is essentially treated like a function pointer, only this time with a strongly defined interface: it takes a single parameter of whatever type you have in your collection, and returns bool.
Regarding where the System.Linq reference comes from and how it is resolved without an explicit #using, it seems that Razor components are actually partial classes, with their other parts "hidden" (meaning autogenerated by the compiler). For your particular example, if you go to the \obj\Debug\netcoreapp3.1\Razor\Pages folder in your project, you will find there a file named "Todo.razor.g.cs", which contains the following:
namespace BlazorApp1.Pages
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
...
[Microsoft.AspNetCore.Components.RouteAttribute("/todo")]
public partial class Todo : Microsoft.AspNetCore.Components.ComponentBase
{
...
}
}
Which means that these 5 namespaces are always available in all components, regardless of the _Imports.razor contents. Not sure why these particular ones have been chosen, that's probably a question for Microsoft.
<label for="client-list" class="label" style="display:inline">Client</label> <i class="fas fa-user-plus"></i> <br />
<input class="input field" value="#(GetClientName(#ClientID))" #oninput="#ClientSearchUpdated"/>
#if (AvailableClients != null)
{
<ul>
#foreach (var client in AvailableClients)
{
<li id="#client.Id" #onclick="#(e => SetClientID(e, client.Id))">#client.FullName</li>
}
</ul>
}
I want to be able to take the above markup/code and turn it into a reusable component that will create an input field that when typed in, will display a list of results based on what is typed in. These results will be of the type of List that is passed in. eg: if a List<Person> is passed in, it would search the DB for people matching the search terms that a user types in. In the generic version, the List holding the objects to be searched wouldn't be AvailableClients, nor the functions getting / updating information specific to clients, of course.
At the end of this, my goal is to be able to replace the above code fragment with:
<SearchableDropdown DropdownItems="AvailableClients"></SearchableDropdown>
(The fields that are searched are currently determined by the sproc used in each of the DataAccessObjects at the moment)
The problem I've come across trying to develop such a generic component is that I'm not super familiar with generics to begin with (I understand the base concepts, what the syntax for using generics is, etc, but I haven't created a lot of generic code), and especially not when it comes to integrating that concept with Blazor.
What I've tried so far is:
Using Inheritance to accept a generic List<ISearchableDropdownItem>, and the objects in my system would implement this Interface, which would have two members: ID and DropdownDisplayInfo which would allow for the dropdown to send information about which item is clicked, as well as give each item something to display for each item in the Search Results.
The problem with this approach is that I then have to create Interfaces for the DataAccess layer & the Services which I've created in my Blazor application as well. This is a long, cascading problem that will lead me down an interface-creation rabbit hole. I'm not even 100% sure if this solution will work in the end.
Use an #typeparam TDropdownItem directive to allow the type used throughout the component to be of that which is passed in.
The obvious problem with this is that it will still put a lot of responsibility on the person utilizing the SearchableDropdown component to give the appropriate markdown for the RenderFragment, plus that still leaves the problem of having generic, "GetObjectName(ObjectID)" and "ObjectSearchUpdated" functions for the component.
Is there a decently straightforward way of implementing this that I'm just completely missing? Am I even remotely on the right track, and it's just going to take a bunch of refactoring of existing code to make things work?
If it is not possible to implement interface approach, maybe you can try create a common proxy object to pass arguments.
For example, you may try to pass Id, Name and Delegate as a proxy list with LINQ.
Prepare your arguments within OnInitialized or wherever you initiate it.
Here is a simple concept which shows how you can use delegation.
Delegation, data and proxy class initialization:
public delegate void OnClick(UIEventArgs eventArgs, int id, string name);
private class ExampleData
{
// let's assume that it has different naming convention for your case
public int IdValue {get;set;}
public string Fullname {get;set;}
}
private class SearchableDropdownItem
{
public int Id {get;set;}
public string Name {get;set;}
public OnClick OnClicked {get;set;}
}
Example proxy creation:
public void Prepare()
{
var dataList = new List<ExampleData>();
for(int i=0; i<3; i++)
{
dataList.Add(new ExampleData(){
IdValue = i+1,
Fullname = "test" + (i + 1)
});
}
var searchableItemList = dataList.Select(x => new SearchableDropdownItem(){
// here is how you can set callback and fields
Id = x.IdValue,
Name = x.Fullname,
OnClicked = new OnClick(SetClientID)
}).ToList();
}
public void SetClientID(UIEventArgs eventArgs, int id, string name)
{
// implement your code
}
And pass searchableItemList to component and use it:
<SearchableDropdown ItemList="searchableItemList"></SearchableDropdown>
...
#foreach (var item in searchableItemList)
{
<li id="#item.Id" #onclick="#(e => item.OnClicked(e,client.Id,client.Name))">#item.Name</li>
}
I have an Item class. I have around 10-20 derivatives of it each containing different types of data. Now when it comes to rendering different types of Item, I'm forced to use likes of:
<div>
#if (Model is XItem)
{
... rendering logic 1 ...
}
#if (Model is YItem)
{
... rendering logic 2 ...
}
#if (Model is ZItem)
{
... rendering logic 3 ...
}
... goes on and on forever ...
</div>
Unfortunately #Html.DisplayFor() does not work in this case because the Model is type of Item, DisplayTemplates\Item.cshtml is displayed.
HTML helpers don't help either because of the same "if/is" chain.
I could incorporate rendering logic inside the classes themselves, and call #Model.Render() but they belong to business logic, not presentation. It would be a sin.
There is only one option of #Html.Partial(Model.GetType().Name) but it feels wrong. You expect a solution without meta-magic. Is there a better way?
Use Display Templates.
Inside your ~/Views/Shared/DisplayTemplates folder you can add a view with the same name as your type.
When you do #Html.DisplayFor(item) you'll get the view related to that specific type.
UPDATE
I just saw your comment RE DisplayFor so if this doesn't help i'll remove my answer.
I think your approach is fine. You can make it better with an extension method like this:
public static MvcHtmlString GetTypePartial<T>(this HtmlHelper htmlHelper, T model, string viewPrefix = "")
{
string typeName = typeof (T).Name;
string viewName = string.Concat(viewPrefix, typeName);
return htmlHelper.Partial(viewName, model);
}
I have been introduced to Razor as applied with MVC 3 this morning, so please forgive me if my question seems terribly uninformed!
I am working with an app whose workflow involves allowing a user to select a value (warehouse) from a drop down list, and add a record (material) from that warehouse to another record (Materials Request). Once the first material has been added to the Materials Request, I need to permanently set the value of the drop down to the warehouse that was first selected, then disable the drop down control (or set to read only, perhaps). The existing code in the razor file uses the DropDownListFor() method, including a ViewBag collection of Warehouse records. I have seen discussions which suggest abandoning the ViewBag design, but honestly I don't have the desire to rewrite major portions of the code; at least it looks like a major rewrite from the perspective of my experience level. Here's the original code:
#Html.LabelPlusFor(m => m.WarehouseId, "*:")
#Html.DropDownListFor(m => m.WarehouseId, (IEnumerable<SelectListItem>)ViewBag.WarehouseCodes, "")<br />
I believe I have been able to select a value based on a session object, though I'm still not sure how to disable the control. Here's my change:
#{
int SelectedWarehouseId = -1;
if (HttpContext.Current.Session["SelectedWarehouseId"] != null)
{
SelectedWarehouseId = Int32.Parse(HttpContext.Current.Session["SelectedWarehouseId"].ToString());
}
}
#Html.LabelPlusFor(m => m.WarehouseId, "*:")
#{
if (SelectedWarehouseId > -1)
{
#Html.DropDownListFor(m => m.WarehouseId, new SelectList((IEnumerable<SelectListItem>)ViewBag.WarehouseCodes, "WarehouseId", "WarehouseDescription", (int)SelectedWarehouseId))<br />
}
else
{
#Html.DropDownListFor(m => m.WarehouseId, (IEnumerable<SelectListItem>)ViewBag.WarehouseCodes, "")<br />
}
}
When the material is added to the Material Request, the WarehouseId is passed to the controller and I can access that value as "model.WarehouseId" in the controller class. However, I'm not sure how to get that value back to the View (apologies for the large code block here):
[HttpPost]
[TmsAuthorize]
public ActionResult Create(ItemRequestViewModel model)
{
string deleteKey = null;
//Removed code
else if (Request.Form["AddToRequest"] != null)
{
// If the user clicked the Add to Request button, we are only
// interested in validating the following fields. Therefore,
// we remove the other fields from the ModelState.
string[] keys = ModelState.Keys.ToArray();
foreach (string key in keys)
{
if (!_addToRequestFields.Contains(key))
ModelState.Remove(key);
}
// Validate the Item Number against the database - no sense
// doing this if the ModelState is already invalid.
if (ModelState.IsValid)
{
_codes.ValidateMaterial("ItemNumber", model.ItemNumber, model.WarehouseId);
Session["SelectedWarehouseId"] = model.WarehouseId;
}
if (ModelState.IsValid)
{
// Add the new Item Request to the list
model.Items.Add(new ItemViewModel() { ItemNumber = model.ItemNumber, Quantity = model.Quantity.Value, WarehouseId = model.WarehouseId });
ModelState.Clear();
model.ItemNumber = null;
model.Quantity = null;
model.WarehouseId = null;
}
}
//Removed code
return CreateInternal(model);
}
private ActionResult CreateInternal(ItemRequestViewModel model)
{
if (model != null)
{
if (!String.IsNullOrEmpty(model.SiteId))
{
ViewBag.BuildingCodes = _codes.GetBuildingCodes(model.SiteId, false);
if (!String.IsNullOrEmpty(model.BuildingId))
ViewBag.LocationCodes = _codes.GetLocationCodes(model.SiteId, model.BuildingId, false);
}
//Removed code
}
//Removed code
ViewBag.WarehouseCodes = _codes.GetWarehouseCodes(false);
return View("Create", model);
}
So my questions are, how do I disable the drop down list, and how can I pass a value for the selected WarehouseId back to the view? I've also considered adding the value to the ViewBag, but to be honest I don't know enough about the ViewBag to recognize any unintended consequences I may face by just randomly modifying it's contents.
Thanks for any help offered on this.
Without going into which approach is better...
Your dropdown should be rendered as an HTML select element, in order to disable this you'll need to add a disabled="disabled" attribute to it.
The DropDownListFor method has a htmlAttributes parameter, which you can use to achieve this:
new { disabled = "disabled" }
when your pass model to your view like
return View("Create", model);
if WareHouseID is set in model then
Html.DropDownListFor(x=>x.WareHouseID, ...)
will automatically set the selected value and u don't have to do that session processing for this. So far as disabling a field is required, stewart is right. you can disable drop down this way but then it won't be posted to the server when u submit the form. you can set it to readonly mode like
new{#readonly = "readOnly"}