How best to make a "composite key" in value field (Blazor) - c#

I am trying to write a component to which it will be possible to pass a list, the elements of which should be clickable and additional information should be displayed when clicked. However, I have a problem with the implementation. Admit: I have a list of groups, I need to get a list of students from all groups (one list for all groups at once). The problem is that students have hardcore identifiers that are unique only WITHIN ONE group. What I mean: in the group "Mathematics" there are students with ID: 1, 2, 4, 9. In the group "Physics" there may also be students with ID: 1, 2, 4, 9, but these are actually other people from another group. Roughly speaking, I can't manipulate non-unique values (I can't give each a unique number to the database - I won't go into details). I can somehow designate uniqueness in #bind by two parameters (group.Id, students.Id)?
<List TOption="VM.Student"
TextField="#(option => option.Name)"
ValueField="#(option => option.Id)" //perfect, if a unique value (this is not the case with me)
#bind-Value="???"
Options="_students" />
Component:
#inherits InputBase<string>
#typeparam TOption
<div>
#if (Options is not null)
{
<div>
#foreach (var option in Options)
{
var text = TextField?.Invoke(option);
var value = ValueField?.Invoke(option);
var isSelected = CurrentValue == value;
<div class="#(isSelected ? " selected" : string.Empty)">
<div #onclick="eventArgs => { OnChange(value, isSelected); }">
}
</div>
}
</div>
#code {
[Parameter] public IEnumerable<TOption> Options { get; set; }
[Parameter] public Func<TOption, string> TextField { get; set; }
[Parameter] public Func<TOption, string> ValueField { get; set; }
protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
{
result = CurrentValue;
validationErrorMessage = string.Empty;
return true;
}
private void OnChange(string value, bool isSelected)
{
if (!isSelected)
{
if (ValueChanged.HasDelegate)
{
ValueChanged.InvokeAsync(value);
}
EditContext?.NotifyFieldChanged(FieldIdentifier);
}
}
}

If I read this correctly:
You have a semi generic component to display a list of items.
You have a unique key problem because the database uses int ids.
I understand why you are trying to use InputBase, but the EditContext confuses me a little, so I may have missed something important!
First data classes. Records (unless you are editing data) to make equality checking simple:
public record StudentKey(int StudentId, int GroupId);
public record Student(StudentKey Uid, string Name, string Grade);
I've mocked a data pipeline into a ViewModel/Presenter DI service:
public class StudentPresenter
{
public IEnumerable<Student> Students = Enumerable.Empty<Student>();
public async ValueTask LoadData()
{
// Fake a real async api/db get
await Task.Delay(100);
var list = new List<Student>();
list.AddRange(MathematicsStudents);
list.AddRange(PhysicsStudents);
this.Students = list;
}
public Student? GetStudent(StudentKey? value)
=> Students.SingleOrDefault(item => item.Uid == value);
private List<Student> MathematicsStudents = new()
{
new(new StudentKey(1, 1), "Fred", "A++"),
new(new StudentKey(2, 1), "Jon", "A"),
new(new StudentKey(3, 1), "Liz", "B-"),
};
private List<Student> PhysicsStudents = new()
{
new(new StudentKey(1, 2), "Clare", "A++"),
new(new StudentKey(2, 2), "Dave", "A"),
new(new StudentKey(3, 2), "Alice", "B-"),
};
}
builder.Services.AddTransient<StudentPresenter>();
I've used an existing component I have to do your list. It's very similar to what you have. It doesn't inherit from InputBase, but does use the binding framework to update a field in the parent component. I've also used the Bootstrap ListGroup to "prettyfy" the display. My original control is actually a select which you can see here in another answer - Blazor pass binding to child component.
#typeparam TItem
#typeparam TValue
<ul class="list-group">
#foreach (var option in this.ItemsProvider)
{
<li class="#this.GetItemCss(option)" #onclick="() => this.OnSelected(option)">#this.GetText(option)</li>
}
</ul>
#code {
[Parameter] public TValue? Value { get; set; }
[Parameter] public EventCallback<TValue> ValueChanged { get; set; }
[Parameter, EditorRequired] public IEnumerable<TItem> ItemsProvider { get; set; } = Enumerable.Empty<TItem>();
[Parameter, EditorRequired] public Func<TItem, string>? TextProvider { get; set; }
[Parameter, EditorRequired] public Func<TItem, TValue>? ValueProvider { get; set; }
private string GetItemCss(TItem value)
{
ArgumentNullException.ThrowIfNull(ValueProvider);
if (this.Value is null)
return "list-group-item";
return this.Value.Equals(ValueProvider(value)) ? "list-group-item active" : "list-group-item";
}
private string GetText(TItem value)
{
ArgumentNullException.ThrowIfNull(TextProvider);
return TextProvider(value);
}
private void OnSelected(TItem value)
{
ArgumentNullException.ThrowIfNull(ValueProvider);
ValueChanged.InvokeAsync(ValueProvider(value));
}
}
And then the demo page:
#page "/"
#inject StudentPresenter Presenter
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<MyDisplay
TItem="Student"
ItemsProvider="this.Presenter.Students"
TValue="StudentKey"
TextProvider="(Student item) => item.Name"
ValueProvider="(Student item) => item.Uid"
#bind-Value=this.studentKey
/>
<div class="alert alert-info m-3">
Student Grade: #this.GetStudentGrade
</div>
#code {
private StudentKey? studentKey;
private string GetStudentGrade => this.Presenter.GetStudent(studentKey)?.Grade ?? "No Student Selected";
protected override async Task OnInitializedAsync()
{
await Presenter.LoadData();
}
}

Considering you have a List of your students allready I would do something like this for the component.
#foreach(var student in Students)
{
<div #onclick="() => StudentClicked(student)">
</div>
}
#code {
[Parameter, EditorRequired]
public List<Stundent> Students { get; set; } = new List<Student>();
private void OnStudentClicked(Student student)
{
// TODO: Make whatever you want with your student object
}
}
If you want to return the student, you should add an EventCallback like this.
#foreach(var student in Students)
{
<div #onclick="() => OnStudentClickedAsync(student)">
</div>
}
#code {
[Parameter, EditorRequired]
public List<Stundent> Students { get; set; } = new List<Student>();
[Parameter, EditorRequired]
public EventCallback<Student> OnStudentClicked;
private async Task OnStudentClickedAsync(Student student)
{
// TODO: Make whatever you want with your student object
await OnStudentClicked.InvokeAsync(student);
}
}

Related

Blazor List of components, shift values from components when removed from list

I'm trying to understand this issue I'm having in blazor 3.1 and appreciate anyone who could explain it to me.
I have a list of items.
I make child components by iterating through the list.
Each component displays a private bool (IsSomething) value that can be toggled with a button. There's a close button (EventCallback) that removes the item from the list in the parent and I can't understand why the bool values of the remaining components shift.
Ex:
[true] [false] [false]
remove 1st item (true in this example) in list gives
[true] [false]
I was expecting
[false] [false]
I would understand if all the bools would be false if the components are re-initialised or something but this behavior has me stumped.
Index.razor
#using ListComponentIssue.Shared
<h1>List Component Issue</h1>
#if (listOfItems != null)
{
<div style="display:flex; flex-direction:row;">
#foreach (var item in listOfItems)
{
<MyComponent ThisItem="#item" Close="close"></MyComponent>
}
</div>
}
Index.razor.cs
{
private List<Item> listOfItems = new List<Item>();
protected override void OnInitialized()
{
base.OnInitialized();
listOfItems.Add(
new Item()
{
Id = "1",
Title = "First"
}
);
listOfItems.Add(
new Item()
{
Id = "2",
Title = "Second"
}
);
listOfItems.Add(
new Item()
{
Id = "3",
Title = "Third"
}
);
listOfItems.Add(
new Item()
{
Id = "4",
Title = "Fourth"
}
);
listOfItems.Add(
new Item()
{
Id = "5",
Title = "Fifth"
}
);
}
public void close(Item itemToRemove)
{
listOfItems.Remove(itemToRemove);
}
}
MyComponent.razor
<div style="margin:5px;">
#ThisItem.Title is #isSomething
</div>
<div>
<button #onclick="toggleBool">Toggle bool</button>
<button #onclick="() => CloseToParent(ThisItem)">close</button>
</div>
</div>
#code {
private bool isSomething { get; set; }
[Parameter]
public Item ThisItem { get; set; }
[Parameter]
public EventCallback<Item> Close { get; set; }
private void toggleBool()
{
isSomething = !isSomething;
}
public async Task CloseToParent(Item itemToClose)
{
await Close.InvokeAsync(itemToClose);
}
public class Item
{
public string Id { get; set; }
public string Title { get; set; }
}
}
In another project, I use Radzen Dropdown multi select components and the selected items from components on the right shift/merge.
ex.:
[selected:1] [selected:2,3]
I remove the first component and the result is
[selected:1,2,3]
Removing components on the right does not change the components on the left.
My goal is for the user to be able to add and remove components dynamically.
Maybe my whole approach is wrong... any advice is appreciated
use #key on your component inside the loop. This helps the render engine know what elements have been updated.
<MyComponent #key=#item ThisItem="#item" Close="close" />
Index.razor
<h1>List Component Issue</h1>
<div style="display:flex; flex-direction:row;">
#foreach (var item in listOfItems)
{
<MyComponent #key=#item ThisItem="#item" OnClose="OnCloseClicked" />
}
</div>
Index.razor.cs
public partial class Index
{
private List<Item> listOfItems;
public Index()
{
listOfItems = new(5)
{
new() {Id = "1", Title = "First"},
new() {Id = "2", Title = "Second"},
new() {Id = "3", Title = "Third"},
new() {Id = "4", Title = "Fourth"},
new() {Id = "5", Title = "Fifth"}
};
}
public void OnCloseClicked(Item itemToRemove)
=> listOfItems.Remove(itemToRemove);
}
MyComponent.razor
<div>
<div style="margin:5px;">
#ThisItem.Title is #isSomething
</div>
<div>
<button #onclick=ToggleBool>Toggle</button>
<button #onclick=CloseToParent>close</button>
</div>
</div>
#code {
private bool isSomething;
[Parameter]
public Item ThisItem { get; set; }
[Parameter]
public EventCallback<Item> OnClose { get; set; }
private void ToggleBool() => isSomething = !isSomething;
public async Task CloseToParent() => await OnClose.InvokeAsync(ThisItem);
}
#key Docs
I wonder if this is the classic c# lambda capture problem. Try
#foreach (var item in listOfItems)
{
var temp = item;
<MyComponent ThisItem="#temp" Close="close"></MyComponent>
}

How to send an object through #change="" (instead of sending value) when using select element

new to Blazor and I have a simple question
In my Blazor app, I have a simple select element:
<select class="form-select" aria-label="Default select example" #onchange="ItemSelected">
<option selected>Open this select menu</option>
#foreach(var item in Items)
{
<option value="#item.Id"> #item.Name </option>
}
</select>
The idea is when a user selects an option, I want to get the object instead of the key, here is the function:
List<object> ItemContainer = new List<object>();
private void ItemSelected(ChangeEventArgs obj) {
...
ItemContainer.Add(obj);
}
How do I capture the object instead?
#page "/"
<select #onchange="ItemSelected" class="form-select">
<option value="null">Select...</option>
#foreach (var option in options)
{
<option #key="option" value="#option.ID">#option.Value</option>
}
</select>
#code {
private Option option;
private List<Option> options = Enumerable.Range(1, 10).Select(i => new Option { ID = i, Value = $"Option{i}" }).ToList();
private void ItemSelected(ChangeEventArgs args)
{
if (int.TryParse(args.Value.ToString(), out int selectedID))
{
option = options.Where(o => o.ID == selectedID).FirstOrDefault();
Console.WriteLine(option.ID.ToString());
Console.WriteLine(option.Value);
}
}
public class Option
{
#nullable enable
public int? ID { get; set; }
#nullable disable
public string Value { get; set; }
}
}
UPDATE:
Hi, sorry for bothering you but I am stuck a bit on something. How do I avoid Option in my code? Because I already have an Item class private IEnumerable Items { get; set; } = new List(); is there a way to substitute both? thanks
Option is the class name given by me. You can give it whatever name you want. You may define your class like the following instead:
public class ItemDTO
{
#nullable enable
public int? Id { get; set; }
#nullable disable
public string Name{ get; set; }
}
Note: If you're using .Net 6.0, you may remove the #nullable directive
Update:
Don't use Task.Run
Change this:
protected override async Task OnInitializedAsync()
{
await Task.Run(GetItems);
Client = _client.Get(Id);
}
private void GetItems()
{
Items = _item.GetAll();
}
To:
protected override void OnInitialized()
{
Client = _client.Get(Id);
Items = _item.GetAll();
}
Change this:
private IEnumerable<ItemDTO> Items { get; set; } = new List<ItemDTO>();
To:
private IList<ItemDTO> Items { get; set; } = new List<ItemDTO>();
The above changes are rather cosmetics than functional. I've no way to verify this as I only read the code; I do not view and run it in an IDE.
Anyhow, the following code snippet is incorrect and as a result leads to an error:
var option = ItemDTO.Where(o => o.Id ==
selectedID).FirstOrDefault();
The code above is suppose to query the the list of ItemDTO objects, and return a single object whose Id property is equivalent to the value we pass to the method (ItemSelected) in which the code is executed. The list of ItemDTO objects, defined as Items should be used for searching the selected item; that is, you should use:
`Items.Where`
Instead of:
ItemDTO.Where
ItemDTO is a class name or a type...
This is how your code should be:
var option = Items.Where(o => o.Id ==
selectedID).FirstOrDefault();
May God bless your code ;}
You can't directly. ChangeEventArgs returns a string as it's Value object.
You need to do something like this:
#page "/"
<select #onchange=OnSelect >
#foreach (var country in Countries)
{
<option value="#country.Id" selected="#this.IsSelected(country)">#country.Name</option>
}
</select>
<div>
Selected : #this.SelectedCountry
</div>
#code {
private Country? country;
private string SelectedCountry => this.country?.Name ?? "None Selected";
protected override void OnInitialized()
{
// to demo selected is working
this.country = this.Countries[1];
}
private void OnSelect(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out int value))
country = Countries.SingleOrDefault(item => item.Id == value);
}
private bool IsSelected(Country c)
=> c == this.country;
public List<Country> Countries = new List<Country>
{
new Country { Id =44, Name = "UK" },
new Country { Id =61, Name = "France" },
new Country { Id =1, Name = "USA" },
};
public class Country
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
}
Sorry I can't type well now to check the syntax exactly, but maybe something like:
#onchange="(e)=> ItemSelected(Items.Single(i=> i.ID.ToString() == e.Value.ToString()))"
And then
private void ItemSelected(YourItemClass SelectedItem) {}

Razor post form not returning object with user inputs

I am trying to update an object in an in memory list, as shown in the PluralSight ASP.NET Fundamentals course. I have run into a problem that when my Update methods is being called from my Edit.cshtml.cs file it is with a default instance of the MeetingSpace class I have created instead of one using the input provided by the user. I have attached the Edit.cshtml and Edit.cshtml.cs files.
If I save the submission, and return to the meeting space table, the top entry is now set to the default object (note the table initially has two rooms named "Meeting Room 1")
Changing the first (or any object) in the list using the browser results in it being replaced by an empty object. I have tested the update method independently and it works as intended and when debugging I can see an default MeetingSpace item being used as the argument in the Update method, so I believe the problem is either with the form or the retrieval of the inputs by the .cs file.
Edit.cshtml
#page "{meetingSpaceId:int}"
#model BookingApp.Pages.MeetingSpaces.EditModel
#{
ViewData["Title"] = "Edit";
}
<h2>Editing #Model.MeetingSpace.Name</h2>
<form method="post">
<input type="hidden" asp-for="MeetingSpace.Id" />
<div class="form-group">
<label asp-for="MeetingSpace.Name"></label>
<input asp-for="MeetingSpace.Name" class="form-control" />
<span class="text-danger" asp-validation-for="MeetingSpace.Name"></span>
</div>
<div class="form-group">
<label asp-for="MeetingSpace.Location"></label>
<input asp-for="MeetingSpace.Location" class="form-control" />
<span class="text-danger" asp-validation-for="MeetingSpace.Location"></span>
</div>
<div class="form-group">
<label asp-for="MeetingSpace.Capacity"></label>
<input asp-for="MeetingSpace.Capacity" class="form-control" />
<span class="text-danger" asp-validation-for="MeetingSpace.Capacity"></span>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>
Edit.cshtml.cs
namespace BookingApp.Pages.MeetingSpaces
{
public class EditModel : PageModel
{
private readonly IMeetingSpaceData meetingSpaceData;
[BindProperty]
public MeetingSpace MeetingSpace { get; set; }
public EditModel(IMeetingSpaceData meetingSpaceData)
{
this.meetingSpaceData = meetingSpaceData;
}
public IActionResult OnGet(int meetingSpaceId)
{
MeetingSpace = meetingSpaceData.GetById(meetingSpaceId);
if (MeetingSpace == null)
{
return RedirectToPage("./NotFound");
}
return Page();
}
public IActionResult OnPost()
{
MeetingSpace = meetingSpaceData.Update(MeetingSpace);
meetingSpaceData.Commit();
return Page();
}
}
}
In my Startup.cs I am using an AddSingleton to update an in memory list, as such:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IMeetingSpaceData, InMemoryMeetingSpaceData>();
services.AddRazorPages();
}
I'm a fairly inexperience programmer and am completely new to ASP.NET so hopefully I have provided you with enough information.
[Edit 1]
MeetingSpace.cs (int id field provided by parent class)
namespace BookingApp.Core
{
public class MeetingSpace : MeetingObjectsBaseClass, IEquatable<MeetingSpace>
{
//private static int classCount = 3; //TODO: Change to 0 default when database implemented
public string Name;
public string Location;
public int Capacity;
public override void EditDetails()
{
throw new NotImplementedException();
}
public bool Equals(MeetingSpace other)
{
if (Name == other.Name
&& Location == other.Location
&& Capacity == other.Capacity)
{
return true;
}
else
{
return false;
}
}
public override int GenerateNewId()
{
throw new NotImplementedException();
}
public override void GetAll()
{
throw new NotImplementedException();
}
}
}
[Edit 2] Parent Classes
MeetingObjectBaseClase
namespace BookingApp.Core
{
public abstract class MeetingObjectsBaseClass : BookingEntityBase
{
public abstract void GetAll();
public override abstract void Update();
}
}
MeetingEntityBase
namespace BookingApp.Core
{
public abstract class BookingEntityBase
{
public int Id { get; set; }
public abstract void EditDetails();
public abstract void GenerateNewId();
}
}
[Edit 3] InMemoryMeetingSpaceData.cs
public class InMemoryMeetingSpaceData : IMeetingSpaceData
{
public List<MeetingSpace> meetingSpaces;
public InMemoryMeetingSpaceData()
{
meetingSpaces = new List<MeetingSpace>()
{
new MeetingSpace { Id = 1, Name = "Meeting Room 1", Location = "The Building", Capacity = 8},
new MeetingSpace { Id = 2, Name = "Meeting Room 2", Location = "The Building", Capacity = 4},
new MeetingSpace { Id = 3, Name = "Meeting Room 3", Location = "The Building", Capacity = 6},
new MeetingSpace { Id = 4, Name = "Meeting Room 4", Location = "The Building", Capacity = 8},
new MeetingSpace { Id = 5, Name = "Meeting Room 1", Location = "Second Building", Capacity = 7}
};
}
public MeetingSpace Add(MeetingSpace newMeetingSpace)
{
if (newMeetingSpace != null)
{
meetingSpaces.Add(newMeetingSpace);
newMeetingSpace.Id = meetingSpaces.Max(m => m.Id) + 1;
}
return newMeetingSpace;
}
public IEnumerable<MeetingSpace> GetAll()
{
return from m in meetingSpaces
orderby m.Name
select m;
}
public MeetingSpace Update(MeetingSpace updatedMeetingSpace)
{
var meetingSpace = meetingSpaces.SingleOrDefault(m => m.Id == updatedMeetingSpace.Id);
if (meetingSpace != null)
{
meetingSpace.Name = updatedMeetingSpace.Name;
meetingSpace.Location = updatedMeetingSpace.Location;
meetingSpace.Capacity = updatedMeetingSpace.Capacity;
}
return meetingSpace;
}
public int Commit()
{
return 0;
}
public MeetingSpace GetById(int id)
{
return meetingSpaces.SingleOrDefault(m => m.Id == id);
}
}

Checkbox List in ASP.NET MVC. I cant retrieve data to a IEnumerable or List

I have a problem with an application using ASP.NET MVC 4, I'm trying to show in a view to create an entity called CompositePiece, a List of items (which are CompositePieces too) from my database, then, the user could select one or more of them, and the items selected would be in a collection in the 'father' CompositePiece, at first, my class CompositePiece:
namespace Garbi.Models{
public class CompositePiece
{
[Key]
public int CompositePieceId { get; set; }
[Required(ErrorMessage = "Introduzca un nombre para la pieza")]
[Display(Name = "Nombre de la pieza")]
public string CompositePieceName { get; set; }
public virtual ICollection<CompositePiece> Components { get; set; }
public int ProcessId { get; set; }
public int LevelOfHierarchy { get; set; }
public CompositePiece(PieceModel model)
{
this.Components = new List<CompositePiece>();
this.CompositePieceName = model.PieceModelName;
LevelOfHierarchy = 0;
}
public CompositePiece()
{
this.Components = new List<CompositePiece>();
LevelOfHierarchy = 0;
}
public CreateOrEditCompositePieceViewModel ToCreateOrEditCompositePieceViewModel(string processName)
{
return new CreateOrEditCompositePieceViewModel
{
CompositePieceName = this.CompositePieceName,
ProcessId = this.ProcessId,
ProcessName = processName
};
}
}
}
I have created a ViewModel to pass the data to a view, the ViewModel is:
namespace Garbi.ViewModels.CompositePieces
{
public class CreateOrEditCompositePieceViewModel
{
[Required(ErrorMessage = "Introduzca un nombre para la pieza")]
[Display(Name = "Nombre de la pieza")]
public string CompositePieceName { get; set; }
public virtual IEnumerable<SelectListItem> Components { get; set; }
public string[] SelectedComponentsId { get; set; }
public int ProcessId { get; set; }
public string ProcessName { get; set; }
public int LevelOfHierarchy { get; set; }
public void AddComponentsList(IEnumerable<CompositePiece> dataProvider, CompositePiece fatherPiece)
{
List<SelectListItem> auxList = new List<SelectListItem>();
foreach (CompositePiece piece in dataProvider)
{
SelectListItem item = new SelectListItem
{
Text=piece.CompositePieceName,
Value = piece.CompositePieceId.ToString(),
Selected = fatherPiece.Components.Contains(piece)
};
auxList.Add(item);
}
Components = new List<SelectListItem>(auxList);
}
public CompositePiece ToCompositePiece()
{
return new CompositePiece
{
CompositePieceName = this.CompositePieceName,
LevelOfHierarchy = this.LevelOfHierarchy,
ProcessId = this.ProcessId
};
}
}
}
Mi View is:
#model Garbi.ViewModels.CompositePieces.CreateOrEditCompositePieceViewModel
#{
ViewBag.Title = "Crear pieza compuesta";
}
<div class="advertise">Crear pieza compuesta para #Model.ProcessName</div>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Nueva pieza compuesta</legend>
<div class="editor-label">
#Html.LabelFor(model => model.CompositePieceName)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.CompositePieceName)
#Html.ValidationMessageFor(model => model.CompositePieceName)
</div>
<div class="editor-field">
#foreach (var item in Model.Components)
{
<div class="checkbox">
#Html.CheckBoxFor(i => item.Selected, item.Value)
#Html.HiddenFor(i => item.Value)
#item.Text
#Html.HiddenFor(i => item)
</div>
}
</div>
#Html.HiddenFor(model => model.Components)
#Html.HiddenFor(model => model.LevelOfHierarchy)
#Html.HiddenFor(model => model.ProcessId)
#Html.HiddenFor(model => model.ProcessName)
<p>
<input type="submit" value="Crear" />
</p>
</fieldset>
<div class="back">
#Html.NavLink("Process_"+#Model.ProcessId, "Page1", "Volver")
</div>
}
And at last, I want to show you the methods of my controller, they are:
public ActionResult ComposePieces(int processId)
{
context = new EFDbContext();
Process process = context.ProcessesPaginable.FindEntityById(processId);
CreateOrEditCompositePieceViewModel model = new CreateOrEditCompositePieceViewModel
{
ProcessId = process.ProcessId,
ProcessName = process.ProcessName,
LevelOfHierarchy = 0
};
IEnumerable<CompositePiece> totalPieces = context.CompositePiecesPaginable.FindAllCompositePiecesOfAProcess(processId).Where(p => p.LevelOfHierarchy == 0);
model.AddComponentsList(totalPieces, model.ToCompositePiece());
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ComposePieces(CreateOrEditCompositePieceViewModel model)
{
context = new EFDbContext();
CompositePiece compositePieceToAdd = model.ToCompositePiece();
CompositePiece aux;
[...]
}
My problem comes when I call the view, I can see perfectly the collection of items, each of them with its checkbox, but when I check some of them, put the CompositePiece's name and click at Submit button, I recover the new CreateOrEditCompositePieceViewModel with its new name, but my attribute Components is empty. I think I am having a mistake of base, but I don't know where is the error, and why I have this problem.
Thank you for your time and sorry for my bad english.
I also have the same Problem as you do.
When I get the postback, the Navigation properties are not returning, even thought I select them on the form, or hid them.
I think this Problem has to do with the Attribute being virtual, and by so it uses lazy loading.
One Thing that I tried that showed promising, but still did^'t get the Job done is if you add the:
#Html.HiddenFor(model => Model.Components);
Before your foreach Loop.
What happens is that when you look on you postback, you will find the components there, it won't be null anymore, but still the list will be empty, but I think the solution is on this direction.
Anyways, enough of this.
I'm about to do a way around this that should work.
What you do, you create a viewmodel
with your class
plus list of each of the virtual lists
something like this:
public class MyViewModel
{
public MyModelView(CompositePiece cp)
{
CompositePiece = cp;
Components = cp.Components.ToList();
}
public CompositePiece CompositePiece { get; set; }
public List<Component> Components { get; set; }
}
I just saw that on your class you create a list of itself, you probably shouldn't do that.
That's not how you create linked lists in C#
So, if instead you do like this example, you will create an instance of you virtual lists, and this should solve the Problem.
Good luck.

Submit form with Get method in MVC3?

I have the following form:
#model Teesa.Models.SearchModel
#using (Html.BeginForm("Index", "Search", FormMethod.Get, new { id = "SearchForm" }))
{
<div class="top-menu-search-buttons-div">
#if (!MvcHtmlString.IsNullOrEmpty(Html.ValidationMessageFor(m => m.SearchText)))
{
<style type="text/css">
.top-menu-search-text
{
border: solid 1px red;
}
</style>
}
#Html.TextBoxFor(q => q.SearchText, new { #class = "top-menu-search-text", id = "SearchText", name = "SearchText" })
#Html.HiddenFor(q=>q.Page)
<input type="submit" value="search" class="top-menu-search-submit-button" />
</div>
<div id="top-menu-search-info" class="top-menu-search-info-div">
Please Select one :
<hr style="background-color: #ccc; height: 1px;" />
<div class="top-menu-search-info-checkbox-div">
#Html.CheckBoxFor(q => q.SearchInBooks, new { id = "SearchInBooks", name = "SearchInBooks" })
<label for="SearchInBooks">Books</label>
</div>
<div class="top-menu-search-info-checkbox-div">
#Html.CheckBoxFor(q => q.SearchInAuthors, new { id = "SearchInAuthors" })
<label for="SearchInAuthors">Authors</label>
</div>
<div class="top-menu-search-info-checkbox-div">
#Html.CheckBoxFor(q => q.SearchInTags, new { id = "SearchInTags" })
<label for="SearchInTags">Tags</label>
</div>
}
and the following Controller and Models :
namespace Teesa.Models
{
public class SearchModel
{
[Required(ErrorMessage = "*")]
public string SearchText { get; set; }
public bool SearchInTags { get; set; }
public bool SearchInAuthors { get; set; }
public bool SearchInBooks { get; set; }
public int Page { get; set; }
public List<SearchBookModel> Result { get; set; }
public List<SimilarBookModel> LatestBooks { get; set; }
}
public class SearchBookModel
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public string Summary { get; set; }
public List<Tags> Tags { get; set; }
public string StatusName { get; set; }
public string SubjectName { get; set; }
public string ThumbnailImagePath { get; set; }
public string BookRate { get; set; }
public string RegistrationDate { get; set; }
public int NumberOfVisit { get; set; }
}
}
[HttpGet]
public ActionResult Index(SearchModel model)
{
FillSearchModel(model);
if (ModelState.IsValid)
{
string page = model.Page;
DatabaseInteract databaseInteract = new DatabaseInteract();
model.Result = new List<SearchBookModel>();
List<Book> allBooks = databaseInteract.GetAllBooks();
List<Book> result = new List<Book>();
#region
if (model.SearchInTags)
{
var temp = (from item in allBooks
from tagItem in item.Tags
where tagItem.Name.Contains(model.SearchText)
select item).ToList();
result.AddRange(temp);
}
if (model.SearchInBooks)
{
var temp = (from item in allBooks
where item.عنوان.Contains(model.SearchText)
select item).ToList();
result.AddRange(temp);
}
if (model.SearchInAuthors)
{
var temp = (from item in allBooks
where item.Author.Contains(model.SearchText)
select item).ToList();
result.AddRange(temp);
}
#endregion
#region Paging
string itemsPerPage = databaseInteract.GetItemsPerPage();
int ItemInPage = int.Parse(itemsPerPage);
var pagingParams = Helpers.SetPagerParameters(page, ItemInPage, result);
ViewBag.AllPagesCount = pagingParams.AllPagesCount;
ViewBag.CurrentPageNumber = pagingParams.CurrentPageNumber;
ViewBag.CountOfAllItems = pagingParams.CountOfAllItems.ToMoneyFormat().ToPersianNumber();
result = pagingParams.ListData as List<Book> ?? result;
#endregion
foreach (var item in result)
{
var bookRate = (item.BookRate == null || item.BookRate.Count == 0)
? 0.0
: item.BookRate.Average(q => q.Rate);
model.Result.Add(new SearchBookModel
{
Author = item.Author,
Id = item.Id,
.
.
.
});
}
}
else
{
model.Result = new List<SearchBookModel>();
}
return View(model);
}
When I submit the form I see the following query strings(Notice the duplicate names) :
http://localhost:2817/Search?SearchText=book&Page=2&SearchInBooks=true&SearchInBooks=false&SearchInAuthors=true&SearchInAuthors=false&SearchInTags=true&SearchInTags=false
But it has to be something like this :
http://localhost:2817/Search?SearchText=book&Page=2&SearchInBooks=true&SearchInAuthors=true&SearchInTags=true
How can I fix it ?
Thanks
Html.Checkbox (and the related For... methods) generate a hidden input for false, and the checkbox for true. This is to ensure that model binding works consistently when binding.
If you must get rid of "false" items resulting from the hidden inputs, you'll need to construct the checkbox inputs yourself (using HTML and not the helper).
<input type="checkbox" id="SearchInBooks" name="SearchInBooks">
Why dont your create a Post Method with a matching name to the Get method. This will ensure that the code is much easier to debug. As you will not have a huge function to go through trying to find problems like this.
I cannot find a where your getting the duplicate url query strings from though.
This will also allow you to bind your results back to the model.
If you want the model binding to happen successfully then you have to go with this way because that is the nature of the Html.CheckBox/Html.CheckBoxFor methods they will render a hidden field as well.
I would suggest rather go with POST to make your life easy. If you still want to use GET then you have to use checkbox elements directly but you have to take care of the model binding issues. Not all the browsers returns "true" when the checkbox is checked for ex. firefox passes "on" so the default model binder throws an error.
Other alternate options is you can go for custom model binder or you can submit the form using jquery by listening to the submit event.

Categories

Resources