C# Blazor Server-Side CSS Element Leak Using InvokeVoidAsync - c#

Trying to hack a transition experience when removing a row from a column. Something like Vue/React. Calling InvokeVoidAsync to append a class that will perform an animation. The javascript call also has a sleep call that will postpone the actual item from being removed on the server side. After the server removes the item, the class is applied to the item below it. Example of a table row appending the class to the next table row UI Side
I created a Blazor Server Side Application and updated the Index.razor page, added a couple js functions, and a css file.
Index.razor
#page "/"
<h2> #Pizzas.Count() Pizza's</h2>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var pizza in Pizzas)
{
<tr id="#(pizza.ID)">
<td>#pizza.Name</td>
<td><button #onclick="#(async _ => await RemovePizza(pizza))">Remove</button></td>
</tr>
}
</tbody>
</table>
#inject IJSRuntime JSRuntime
#code {
public record Pizza(Guid ID, string Name);
public string[] PizzaTypes => new[] { "Pepperoni", "Cheese", "Hawaiian", "Veggie", "Bacon", "Southwestern BBQ", "Cheeseburger", "Buffalo", "Meat", "Supreme" };
public List<Pizza> Pizzas { get; set; } = new();
protected override void OnInitialized()
{
base.OnInitialized();
try
{
for (int i = 0; i < 10; i++)
AddPizza();
}
catch (Exception)
{
throw;
}
StateHasChanged();
}
public void AddPizza()
{
Pizzas.Add(new Pizza(Guid.NewGuid(), PizzaTypes[new Random().Next(0, PizzaTypes.Length - 1)]));
}
public async Task RemovePizza(Pizza pizza)
{
await JSRuntime.InvokeVoidAsync("animateTransition", pizza.ID, "fade-away", 1000);
Pizzas.Remove(pizza);
}
}
JS Functions
var sleep = async (ms) => new Promise(resolve => setTimeout(resolve, ms));
var animateTransition = async (id, className, duration) => {
const el = document.getElementById(id);
el.classList.add(className);
await sleep(duration);
};
CSS
#keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
tr.fade-away {
animation-name: fadeOut;
animation-duration: 1s;
}

Turns out I was missing the #key attribution within the loop. Such a novice mistake.
https://blazor-university.com/components/render-trees/optimising-using-key/
Updated Code
#foreach (var pizza in Pizzas)
{
<tr #key="pizza" id="#(pizza.ID)">
<td>#pizza.Name</td>
<td><button #onclick="#(async _ => await RemovePizza(pizza))">Remove</button></td>
</tr>
}

Related

Foreach Loop in Razor Component causes await to fail

I'm new to Blazor and .NET 6, but I am working on a small app. I have LiteDB storing data on the backend, and my Razor component is fetching it. The problem is that, while 80% of the time it works as expected, the other 20% of the time the data gets populated as null in the Razor component before the database call has even processed. When debugging this, I see the code get to the database call in the backend, then it jumps back to the frontend Razor component which now has a null object, and then it jumps back to the backend where the database call has now been completed. The page populates from the null object though, so the data is not there. I get an error in the console, but it's just stating that the object was not set to an instance of an object at a foreach loop in my code. Here's my code:
#page "/origin"
#page "/origin/{id:int}"
#inject HttpClient http
#if (id == null)
{
<PageTitle>Origin Not Found</PageTitle>
<h3>Origin Not Found</h3>
}
else
{
<PageTitle>Origin: #origin.Name</PageTitle>
<h3>Origin: #origin.Name</h3>
<table class="table">
<tbody>
<tr>
<td>Description: </td>
<td>#origin.Description</td>
</tr>
<tr>
<td>Starting Health: </td>
<td>#origin.StartingHealth</td>
</tr>
<tr>
<td>Ground Movement Speed: </td>
<td>#origin.GroundMovementSpeed</td>
</tr>
#foreach(var stat in #essences)
{
<tr>
<td>Essence Increase Option: </td>
<td>#stat</td>
</tr>
}
<tr>
<td>Origin Benefit: </td>
<td>#origin.OriginBenefit</td>
</tr>
</tbody>
</table>
}
#code {
[Parameter]
public int id { get; set; }
private Origin origin = new();
private List<Essence> essences = new();
protected async override Task OnInitializedAsync()
{
var result = await http.GetFromJsonAsync<Origin>($"/api/origin/{id}");
if(result != null)
{
origin = result;
essences = result.EssenceIncreaseOptions;
}
}
}
Now if I remove the foreach loop (and the essence information I want there), then the code works 100% of the time. So there's something about that foreach loop that's causing it to fail 20% of the time, but I'm just not sure what. Any help would be appreciated. Thanks in advance.
In the posted code, the only point in the foreach(var stat ...) that could yield an NRE is <td>#stat</td>
So your Essence class probably has an override ToString() that causes the null de-reference.
What you can do is put a conditional check before the #foreach loop and when essences is null show some message saying loading etc, also, put StateHasChanged() after completion of fetching of data.
#if(essences.Count>0)
{
#foreach(var stat in #essences)
{
<tr>
<td>Essence Increase Option: </td>
<td>#stat</td>
</tr>
}
}
else {loading data}
and
protected async override Task OnInitializedAsync()
{
var result = await http.GetFromJsonAsync<Origin>($"/api/origin/{id}");
if(result != null)
{
origin = result;
essences = result.EssenceIncreaseOptions;
StateHasChanged();
}
}

Component attributes do not support complex content (mixed C# and markup)

I am trying to use a Razor argument and pass it into Blazor for further processing, but I get this error message "Component attributes do not support complex content (mixed C# and markup)" on the #onclick event I am trying to build on the img tag below:
<tr>
#{
for (int j = 0; j < Candidates.Length; j++)
{
<th>
<div class="grow">
<img src="/Candidates/#(Candidates[j].ToString()).jfif" alt="#Candidates[j].ToString()" #onclick="IncrementScore(#j)" />
</div>
</th>
}
}
</tr>
Any suggestions would be greatly appreciated!
The main issue with your code because of which you've got a compiler error is the way you call the IncrementScore method. You should realize that #onclick is not an Html attribute to which you should assign a value, in this case a method that gets a value.
The #onclick attribute is a compiler directive, instructing the compiler how to form an event handler that should be invoked when the element is clicked, the target of the event, etc. In your case, you wish to call a method and pass it a value. This can only be done by using a lambada expression as follows:
#onclick="#(()=> IncrementScore(<value to pass to the method>))"
The following code snippet illustrates how to call the IncrementScore method properly when you're using a for loop or foreach loop. The distinction is very important regarding local variables that are passed to methods in loops
You can put the following code in the Index component, and run it as is:
#*#for (int j = 0; j < Candidates.Count; j++)
{
int localVariable = j;
<img src="#Candidates[j].Src" #onclick="#(()=>
IncrementScore(localVariable))" />
}*#
#foreach (var candidate in Candidates)
{
Random random = new Random();
<img src="#candidate.Src" #onclick="#(()=>
IncrementScore(random.Next(0,9)))" />
}
<p>#Scores.ToString()</p>
#code {
List<Candidate> Candidates = Enumerable.Range(1, 10).Select(i => new
Candidate { CandidateName = i }).ToList();
private int Scores;
private Task IncrementScore(int score)
{
Scores = score;
return Task.CompletedTask;
}
public class Candidate
{
public int CandidateName { get; set; }
public string Src => $"{CandidateName}.jfif";
}
}
Hope this helps...
You may also encapsulate the complex content in a code method:
<NavLink href="#GetLink()" class="button is-link">
Details
</NavLink>
#code {
[Parameter]
public string? Id { get; set; }
private string GetLink()
{
return this.Id != null ? $"/details/{this.Id}" : "#";
}
}
I came to this page in search for a solution for the same error:
Component attributes do not support complex content (mixed C# and markup)
In my case was all about the href link construction. I followed your solution on the accepted answer comment and it worked. Thanks #Øyvind Vik
Just in case it help others i'll leave my code as an example.
foreach (List<string[]> lss in searchResults)
{
var link = $"genericform/{entidadeSelecionada}/{lss.FirstOrDefault()[1]}";
<tr>
#foreach (string[] st in lss)
{
if (camposAListar.Contains(st[0].ToLower()))
{
<td>#st[1]</td>
}
}
<td>
<NavLink href="#link">
<span class="oi oi-list-rich" aria-hidden="true"></span> Utilizador
</NavLink>
</td>
</tr>
}

ViewModel Property Null in Post Action

This is now fixed. A combination of Ish's suggestion below plus adding calls to #HiddenFor in the view resolved the problem.
I have an ASP.NET MVC 5 web application where users can mark a defect as resolved. I want to display a list of potentially related defects, with check-boxes that users can tick to indicate that yes, this is the same defect, and should also be marked as resolved.
So I have a View Model with a property that is a collection, each member of which contains a defect object property and Boolean IsSameDefect property. This all works fine in the GET action method and in the view. I can display the related defects and tick the boxes.
The problem arises in the POST action when I want to update the data. At this point the property (the collection of potentially related defects) is null. I'm having a hard time trying to figure out how to pass this data back to the controller?
Code as requested ...
// GET: /DefectResolution/Create
public ActionResult Create(int ciid)
{
int companyId = User.CompanyID();
DefectResolutionCreateViewModel drcvm = new DefectResolutionCreateViewModel(ciid, companyId);
return View(drcvm);
}
// POST: /DefectResolution/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(DefectResolutionCreateViewModel drcvm)
{
DefectResolutions currentResolution = drcvm.DefectResolution;
currentResolution.CreatedOn = System.DateTime.Now;
currentResolution.UserID = User.UserID();
if (ModelState.IsValid)
{
unitOfWork.DefectResolutionRepository.Insert(currentResolution);
if (currentResolution.ResolutionStatusID == 2)
{
//code breaks here as drcvm.RelatedUnresolvedDefects is null
foreach (var relatedDefect in drcvm.RelatedUnresolvedDefects)
{
if (relatedDefect.IsSameDefect)
{
DefectResolutions relatedResolution = new DefectResolutions();
relatedResolution.ChecklistID = relatedDefect.RelatedChecklist.ChecklistID;
relatedResolution.CreatedOn = System.DateTime.Now;
relatedResolution.ResolutionNote = currentResolution.ResolutionNote;
relatedResolution.ResolutionStatusID = currentResolution.ResolutionStatusID;
relatedResolution.UserID = User.UserID();
}
}
}
unitOfWork.Save();
return RedirectToAction("Index", new { ciid = currentResolution.ChecklistID });
}
return View(drcvm);
}
In the view ...
#model Blah.ViewModels.DefectResolution.DefectResolutionCreateViewModel
#{
ViewBag.Title = "Create Defect Resolution";
var relatedDefects = Model.RelatedUnresolvedDefects;
}
... and later in the view ...
#for (int i = 0; i < relatedDefects.Count(); i++ )
{
<tr>
<td>
#Html.EditorFor(x => relatedDefects[i].IsSameDefect)
</td>
</tr>
}
I followed Ish's suggestion below, and modified the code to refer to Model.RelatedUnresolvedDefects directly instead of using a variable as I had been doing. This does get me a bit further. The view model's RelatedUnresolvedDefects property is no longer null. But only RelatedUnresolvedDefects.IsSameDefect has a value. RelatedUnresolvedDefects.RelatedChecklist is null. Here's the controller code again showing where it now breaks ...
// POST: /DefectResolution/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(DefectResolutionCreateViewModel drcvm)
{
DefectResolutions currentResolution = drcvm.DefectResolution;
currentResolution.CreatedOn = System.DateTime.Now;
currentResolution.UserID = User.UserID();
if (ModelState.IsValid)
{
unitOfWork.DefectResolutionRepository.Insert(currentResolution);
if (currentResolution.ResolutionStatusID == 2)
{
//prior to change, code used to break here
foreach (var relatedDefect in drcvm.RelatedUnresolvedDefects)
{
if (relatedDefect.IsSameDefect)
{
DefectResolutions relatedResolution = new DefectResolutions();
//code now breaks here because relatedDefect.RelatedChecklist is null
relatedResolution.ChecklistID = relatedDefect.RelatedChecklist.ChecklistID;
relatedResolution.CreatedOn = System.DateTime.Now;
relatedResolution.ResolutionNote = currentResolution.ResolutionNote;
relatedResolution.ResolutionStatusID = currentResolution.ResolutionStatusID;
relatedResolution.UserID = User.UserID();
}
}
}
unitOfWork.Save();
return RedirectToAction("Index", new { ciid = currentResolution.ChecklistID });
}
return View(drcvm);
}
Without knowing your code.I suggest you to use for loop instead of foreach while rendering the defects in View (.cshtml).
Editing Answer based on your code.
Following statement in the view creating problem
var relatedDefects = Model.RelatedUnresolvedDefects;
You should directly iterate over the Model.RelatedUnresolvedDefects property in the loop.
#for (int i = 0; i < Model.RelatedUnresolvedDefects.Count(); i++ )
{
<tr>
<td>
#Html.EditorFor(x => Model.RelatedUnresolvedDefects[i].IsSameDefect)
</td>
</tr>
}

In MVC 4 How do I Add multiple Collections to a session?

I'm using MVC 4 with Razor Syntax to create a collection based on a class that was created using scaffolding (Database first based development) and I can add the first collection to the Session and return it to the Index view and display it on the page.
When I attempt to add a second collection to the Session Variable it gives me a error.
Unable to cast object of type
'System.Collections.Generic.List`1[EagleEye.Models.tblTask]' to type
'EagleEye.Models.tblTask'.
What am I doing wrong - how do I add 2 collections to the session?!
Index.cshtml (My Index view using Razor syntax)
#model List<myApp.Models.tblTask>
<table>
#{
foreach (var tblTask in Model)
{
<tr>
<td>
TaskName: #tblTask.Name
</td>
<td>
Desc: #tblTask.Description
</td>
<td>
Schedule: #tblTask.Freq #tblTask.FreqUnit
</td>
<td>
Reocurring?: #tblTask.ReocurringTask.ToString()
</td>
</tr>
}
}
</table>
Here's the "ActionResult" portion of the code from my HomeController.cs:
[HttpPost]
public ActionResult CreateTask(tblTask newTask)
{
var TaskCollection = new List<tblTask>();
if (Session["TaskCollection"] != null)
{
TaskCollection.Add((tblTask)Session["TaskCollection"]);
}
TaskCollection.Add(newTask);
Session["TaskCollection"] = TaskCollection;
return RedirectToAction("Index");
}
public ActionResult Index()
{
var TaskCollection = new List<tblTask>();
if (Session["TaskCollection"] != null)
{
TaskCollection = (List<tblTask>)Session["TaskCollection"];
}
return View(TaskCollection);
}
When I add the first entry it works fine and shows up on my index view. When I try to add the second collection of tasks, it tells me:
Unable to cast object of type
'System.Collections.Generic.List`1[EagleEye.Models.tblTask]' to type
'EagleEye.Models.tblTask'.
I've been fighting this for a few days now and have been developing for a while, but am just beginning to learn the power of asking questions when I'm stumped (instead of just continuing to beat my head against the wall until something caves in (often my head), so if my question is not well formed, please let me know.
Thanks!
Dan
Because, inside your if condition, you are casting the Session["TaskCollection"](which is a collection of tblTask to a single instance of tblTask.
This should work.
[HttpPost]
public ActionResult CreateTask(tblTask newTask)
{
var TaskCollection = new List<tblTask>();
//Check whether the collection exist in session, If yes read it
// & cast it to the tblTask collection & set it to the TaskCollection variable
if (Session["TaskCollection"] != null)
{
TaskCollection= (List<tblTask>) Session["TaskCollection"];
}
if(newTask!=null)
TaskCollection.Add(newTask);
//Set the updated collection back to the session
Session["TaskCollection"] = TaskCollection;
return RedirectToAction("Index");
}
I finally see the light -- Note the change in the HomeController.cs "TaskCollection = (List)Session["TaskCollection"]; "
[HttpPost]
public ActionResult CreateTask(tblTask newTask)
{
var TaskCollection = new List<tblTask>();
if (Session["TaskCollection"] != null)
{
//Here is the line that changed -- the following line works~
TaskCollection = (List<tblTask>)Session["TaskCollection"];
}
TaskCollection.Add(newTask);
Session["TaskCollection"] = TaskCollection;
return RedirectToAction("Index");
}

ASP.NET MVC 3 - multiple checkbox list

I read tutorial "Creating an Entity Framework Data Model for an ASP.NET MVC Application" from http://www.asp.net/ and wanted to use multiple checkbox list from part 6 - http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/updating-related-data-with-the-entity-framework-in-an-asp-net-mvc-application
In this tutorial, it is only possible to access these checkbox options (to assign courses to each instrucor) from edit page of instructor. I want to render these checkboxes on the create page, but couldn't adjust codes I've found in this tutorial.
These are parts of the codes that are used to display and use checkboxes for new inputs to database.
HttpGet Edit method
public ActionResult Edit(int id)
{
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.InstructorID == id)
.Single();
PopulateAssignedCourseData(instructor);
return View(instructor);
}
private void PopulateAssignedCourseData(Instructor instructor)
{
var allCourses = db.Courses;
var instructorCourses = new HashSet<int>(instructor.Courses.Select(c => c.CourseID));
var viewModel = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
viewModel.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
ViewBag.Courses = viewModel;
}
HttpPost Edit method
[HttpPost]
public ActionResult Edit(int id, FormCollection formCollection, string[] selectedCourses)
{
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.InstructorID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))
{
try
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
db.Entry(instructorToUpdate).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
PopulateAssignedCourseData(instructorToUpdate);
return View(instructorToUpdate);
}
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}
var selectedCoursesHS = new HashSet<string>(selectedCourses);
var instructorCourses = new HashSet<int>
(instructorToUpdate.Courses.Select(c => c.CourseID));
foreach (var course in db.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Remove(course);
}
}
}
}
And the view
<div class="editor-field">
<table>
<tr>
#{
int cnt = 0;
List<ContosoUniversity.ViewModels.AssignedCourseData> courses = ViewBag.Courses;
foreach (var course in courses) {
if (cnt++ % 3 == 0) {
#: </tr> <tr>
}
#: <td>
<input type="checkbox"
name="selectedCourses"
value="#course.CourseID"
#(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
#course.CourseID #: #course.Title
#:</td>
}
#: </tr>
}
</table>
</div>
So, I want to insert multiple choices (select choices) to database, but would like to display this list of checkboxes immediately on the create page, not the edit page like it was done in the tutorial. If you have another solution that is not related to this tutorial, I would also be grateful. I know that I can't use these methods because they're methods for Edit, so they're takin "id" as parameter, but I hope that I can adjust them to be used in create page of my entity.
Consider using a partial view to isolate the control you're trying to render on both pages, you just need to make sure your model lines up with what the model for your view is.
http://mvc4beginner.com/Tutorial/MVC-Partial-Views.html

Categories