Access individual items in Blazor Virtualized list - c#

I have a simple collection of "Conversation" like this:
public class Conversation
{
public string? contactName { get; set; }
public string? thread { get; set; }
public DateTime newestMsg { get; set; }
}
I was originally using #foreach to put these onto a page to display as a scrollable list. I'd load 30 of them from an API call and populate them into "ConversationList":
#foreach (var conv in ConversationList)
{
<div class="conversation">
<div>#conv.contactName</div>
<div>#conv.thread</div>
<div>#conv.newestMsg</div>
</div>
}
#code {
Conversation[]? ConversationList = new Conversation[] { };
}
I saw Blazor has the Virtualize component which would let me have tons of conversations be scrollable and load on demand, so I changed my code to use it:
<Virtualize ItemsProvider="#LoadConversations" Context="conv">
<ItemContent>
<div class="conversation">
<div>#conv.contactName</div>
<div>#conv.thread</div>
<div>#conv.newestMsg</div>
</div>
</ItemContent>
</Virtualize>
#code {
private async ValueTask<ItemsProviderResult<Conversation>> LoadConversations(ItemsProviderRequest request)
{
string apiURL = "https://myAPI:00000?start=" + request.StartIndex + "&limit=" + request.Count;
ConversationRequest? convRequest = await Http.GetFromJsonAsync<ConversationReq>(apiURL);
if (convRequest != null && convRequest.success)
{
return new ItemsProviderResult<Conversation>(convRequest.Conversations, request.StartIndex + request.Count + 4);
}
else
{
return new ItemsProviderResult<Conversation>(null, 0);
}
}
}
In my first attempt, I could select a specific item in "ConversationList" and modify it's contents, having the state change on the page. For example, if a new message came in from SignalR, I could select a Conversation by it's "thread" property and update the "newestMsg" datetime.
Now that I have them Virtualized instead, it's not clear how to do this. I can't use Virtualize's "ItemsProvider" and "Items" at the same time. I want to keep the ItemsProvider for the sake of automatic loading while scrolling, knowing the StartIndex and Count automatically.

Related

Why isn't my list maintaining state on the PageModel between posts?

I'm maintaining a list on my PageModel as a BindProperty.
When the user submits a form on the page, this list should be added to and the input from which it got its value should be reset.
<form class="ratingQuestionForm" method="post" asp-page-handler="AddQuestion">
<input type="hidden" name="questionType" id="questionType" value="Rating" />
<textarea name="questionText" id="questionText" class="form-control mb-1"
required></textarea>
</form>
<a class="btn btn-outline-dark add-rating-question">Add Rating
Question</a>
<script>
$(".add-rating-question").on("click", function () {
$(".ratingQuestionForm").submit();
$(this).closest("textarea").val("");
});
</script>
On the backend, I'm using TempData["Questions"] as short term storage for the list of questions submitted this way.
[BindProperties]
public class CreateModel : PageModel
{
public List<Question> Questions { get; set; }
// The usual constructor and DI stuff has been removed for brevity.
public async Task OnGetAsync()
{
// If there are already questions in ViewData, load them.
if (ViewData["Questions"] is not null)
{
this.Questions = ViewData["Questions"] as List<Question>;
}
// Basically everything not related to the question removed for brevity.
}
public void OnPostAddQuestion(string type, string text)
{
var _type = type switch
{
"Rating" => QuestionType.Rating,
_ => QuestionType.OpenEnded
};
// Load existing questions if there are any.
if (ViewData["Questions"] is not null)
{
this.Questions = ViewData["Questions"] as List<Question>;
}
this.Questions.Add(new Question
{
Type = _type,
QuestionText = text
}
}
}
When I wasn't seeing the behavior I wanted on the UI, I had the page build a list of questions for me. This indicated that only the last question added was persisting to the model.
#if (Model.Questions is not null && Model.Questions.Any())
{
<ul class="list-group list-group-flush">
#foreach (var q in Model.Questions)
{
<li class="list-group-item">#q.QuestionText</li>
}
</ul>
}
How can I persist my questions through posts?
In your question I see that you say "TempData", but in your code I see you using "ViewData" instead. I believe ViewData only lasts for the current request. What you'll want to use is TempData. Please note, however, that the TempData dictionary cannot hold complex objects. To get around this, you can serialize and de-serialize whatever you need storing. I'd recommend using JavaScript Object Notation (JSON). You can use the System.Text.Json namespace/library discussed in this article for serializing:
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-overview?pivots=dotnet-core-3-1
Here's what I did with my own code to try and solve your issue:
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.Text.Json;
namespace StackOverflowQA.Pages
{
public class CreateModelModel : PageModel
{
public class Question
{
public string Type { get; set; }
public string Text { get; set; }
}
private const string questionsKey = "Questions";
public List<Question> Questions { get; set; }
public void OnGet()
{
Questions = GetQuestions();
}
public void OnPostAddQuestion(string type, string text)
{
Questions = GetQuestions();
Questions.Add(new Question { Type = type, Text = text });
TempData[questionsKey] = JsonSerializer.Serialize(Questions);
}
private List<Question> GetQuestions()
{
TempData.TryGetValue(questionsKey, out object o);
if (o is null)
return new List<Question>();
return JsonSerializer.Deserialize<List<Question>>((string)o);
}
}
}
Here's another article found that explains a bit about how TempData works:
https://www.learnrazorpages.com/razor-pages/tempdata

add list of dynamic components in blazor

I just started to have a look in blazor (v0.3) and doing some test I wanted to add a list using blazor
First I created a List<string> to test a simple list in the same page
<ul>
#foreach (var item in listItems)
{
<li>#item</li>
}
</ul>
#functions {
private List<string> listItems = new List<string>();
private string newItem;
private void AddItem()
{
if (string.IsNullOrEmpty(newItem))
return;
listItems.Add(newItem);
newItem = "";
}
}
this is working fine, is adding every element to the list when I add it. but then, i tried to add components, add a single component was easy, based on this question here but for a list I had the next problem:
I created a <li> compontent just to test the functionality of components, here is the component view
<li id="#ID">
#Text
</li>
#functions {
[Parameter]
string Text { get; set; }
[Parameter]
string ID { get; set; }
}
then in the parent view
<input type="text" bind="TxtExample" name="inpAdd"/>
<button onclick="#addCompoment">add comp1</button>
<div class="simple-list-list">
#if (!componentListTest.Any())
{
<p>You have no items in your list</p>
}
else
{
<ul>
#foreach (var item in componentListTest)
{
#item
}
</ul>
}
</div>
#functions {
private List<RenderFragment> componentListTest { get; set; }
private int currentCount {get; set;}
private string TxtExample { get; set; }
protected override void OnInit()
{
currentCount = 0;
componentListTest = new List<RenderFragment>();
}
protected void addCompoment()
{
componentListTest.Add(CreateDynamicComponent(currentCount));
currentCount++;
}
RenderFragment CreateDynamicComponent(int counter) => builder =>
{
var seq = 0;
builder.OpenComponent(seq, typeof(listExample));
builder.AddAttribute(++seq, "Text", "text -- "+TxtExample);
builder.AddAttribute(++seq, "id","listed-"+counter);
builder.CloseComponent();
};
}
when I load the fist element is loaded correctly:
but when I entered the second one, all of them are replaced for the last one:
Any idea whats going on?
You are making it too complicated. You don't need to dynamically instantiate components for this scenario to work.
You can just do a:
<ul>
#foreach (var item in listItems)
{
<myComponent bind-myVar="#item"></myComponent>
}
</ul>
And the components will be instantiated for you.
Also see here how to make the parameters work on your component.
This is because TxtExample is global to the component. When Blazor detects a potential UI change, it recalculates the entire component and updates the DOM with any differences. So when you change the textbox, TxtExample is updated and then the Razor is recalculating, inserting the new value of TxtExample for all rows.

How to validate dynamic controls in .net core

I am creating the survey application for which I am creating the controls dynamically(Like checkbox,radio-button,textbox etc).
Each question will have the controls depending upon the control type assigned to the question and on question type the answer choices(checkbox, radio button) will be rendered.
On Next/Previous navigation I am storing current page answers in the database. While navigating the page I am doing ajax call for database saving and my UI/controls are NOT in the form.
I have created ViewModel based on my LMS_SurveyQuestions and LMS_SurveyQuestionOptionChoice table.
So, while creating the UI in view in for loop, I have directly assigned SurveyQuestionOptionChoiceID as Control ID while creating the AnswerChoice controls and stored the same in the table SurveyUserAnswer table.
Model
public class LMS_TraineeSurveyPaginationViewModel
{
public List<LMS_SurveyQuestions> SurveyQuestions { get; set; }
public List<LMS_SurveyQuestionOptionChoice> SurveyQuestionOptionChoice { get; set; }
public SurveyPager Pager { get; set; }
}
and this is how I rendered the view
#foreach (var item in Model.SurveyQuestions)
{
foreach (var data in Model.SurveyQuestionOptionChoice.Where(x => x.SurveyQuestionID == item.SurveyQuestionID).ToList())
{
if (item.QuestionTypeID == QuestionType.RadioButton)
{
<li style="list-style:none;">
<input type="radio" name="rb" id="#data.SurveyQuestionOptionChoiceID" />
<span>#data.OptionChoice</span>
</li>
}
else if (item.QuestionTypeID == QuestionType.CheckBox)
{
<li style="list-style:none;">
<input type="checkbox" id="#data.SurveyQuestionOptionChoiceID" name="#data.SurveyQuestionOptionChoiceID" " />
<span>#data.OptionChoice</span>
</li>
}
}
}
and while saving the answer into database I have created the JSON/JS array as model for SurveyUserAnswer and saved it into database as follows. Below is example for radio button
function SaveValues() {
var surveyQuestion = #Html.Raw(Json.Serialize(Model.SurveyQuestions.ToArray()));
var surveyQuestionOptionChoide = #Html.Raw(Json.Serialize(Model.SurveyQuestionOptionChoice.ToArray()));
for (item in surveyQuestion) {
var surveyQuestionID=surveyQuestionViewModel[item].SurveyQuestionID;
var filteredData = surveyQuestionOptionChoide.filter(function(filteredItem) {
return (filteredItem.SurveyQuestionID==surveyQuestionID);
});
for (optionChoice in filteredData) {
if(surveyQuestion[item].QuestionTypeID=='#QuestionType.RadioButton') {
if (($('#'+SurveyQuestionOptionChoiceID).prop("checked"))) {
surveyUserAnswer.push({ SurveyUserAnswerID: filteredData[optionChoice].SurveyUserAnswerID==null?0:filteredData[optionChoice].SurveyUserAnswerID,
SurveyQuestionOptionChoiceID: SurveyQuestionOptionChoiceID,SurveyUserID:'#ViewBag.SurveyUserID',AnswerText:null,
MarksObtained:filteredData[optionChoice].Marks,WhenCreated:'#DateTime.UtcNow',WhoCreated:'#tenant.UserID'});
}
}
}
}
$.post('#Url.Action("GetTraineeSurvey", "Survey")', {SurveyID:surveyID,page:page, surveyUserAnswer: surveyUserAnswer,PrevBranchQuestionPage:currentPage,IsBranchQuestionAvailable:IsBranchQuestionAvailable }, function (data) {
$('#surveyModalContent').html('');
$('#surveyModalContent').html(data);
$("#surveyModal").modal('show');
}).fail(function() {
alert( "error in GetTraineeSurvey" );
}).success(function() { });
}
So, my question is how can I validate dynamically created controls in this scenario ?
You can use Unobtrusive jQuery validation basic on data attributes on controls.
Read more about that on this LINK.

How to add #Html.ValidationMessageFor for each item in a collection?

How would you add #Html.ValidationMessageFor() for each item in a collection? Say,
public class FooVm
{
// some property
public ICollection<BarVm> Bars { get; set; }
}
public class BarVm
{
// some property
[Range(1, int.Max, ErrorMessage = "Must be greater than 1")
public float? Fox { get; set; }
}
Then in a view
#model namespace.here.FooVm
<div class="container"></div>
Populate
<script>
$(function() {
var i = 0;
var populate = function() {
var strBuilder = '<input type="text" name="Bars[i].Fox" />';
$(".container").append(strBuilder);
return false;
};
$(".trigger").click(populate);
});
</script>
It's all working. But how can I add the validation in every textbox? I'm using ASP.NET MVC 4 still practicing. I'm also utilizing unobtrusive validation for client validation. Any you-should-do-something-like-this suggestions or tips, sample code would be great. Thanks.
Actually, using Javascript to populate a View is not the way MVC should be used. Instead, you can render all textboxes like this:
First the code for the class:
public class FooVm
{
// some property
public List<BarVm> Bars { get; set; }
public FooVm()
{
// Make sure the collection exists to prevent NullReferenceException
this.Bars = new List<BarVm>();
}
}
public class BarVm
{
// some property
[Range( 1, Int32.MaxValue, ErrorMessage = "Must be greater than 1" )]
public float? Fox { get; set; }
}
Now the code for the View:
#model WebApplication2.Models.FooVm
<h2>Sample View</h2>
#using ( Html.BeginForm( "YourAction", "YourController" ) )
{
<div class="container">
#for ( int i = 0; i < Model.Bars.Count; i++ )
{
#Html.TextBoxFor( m => m.Bars[i].Fox )
#Html.ValidationMessageFor( m => m.Bars[i].Fox );
}
</div>
}
This will render the necessary tags - and of course the validationmessage-bits. However, it's also possible to combine all error messages in one place by using
#Html.ValidationSummary()
If you really want to display the stuff only after clicking a button, consider using a partial view and loading that one. That's a much better approach than trying to create all necessary tags and attributes for validation using javascript.
Regards,
Frank

Sending Complex Data to ASP MVC view

I've been struggling to research an answer to this question as I cannot come up with the correct search terms.
Basically I have 2 IEnumerable<T>'s in my controller, below is the code for the attempt I made.
Controller:
IEnumerable<Room> allRooms = RoomHelper.FindAllRooms();
foreach (var room in allRooms)
{
IEnumerable<Bunk> associatedBunks = RoomHelper.FindAssociatedBunksByRoom(room);
if (associatedBunks.Count() > 0)
{
ViewData["Room_"+room.RoomId] = associatedBunks;
}
}
And I'm trying to send them to the view in a way that I can do two foreach loops that will cycle through one set of data (in this case the Room objects and will then using the Room.RoomId key cycle through another IEnumerable which contains the associated data.
My view looks like this but is showing parse errors:
#foreach (var room in ViewBag.Rooms)
{
<h2>#room.RoomName</h2>
#if (ViewData["Room_" + room.RoomId].Count() > 0)
{
<ol>
#foreach (var bunk in ViewData["Room_" + room.RoomId])
{
<li>#bunk.BunkId</li>
}
</ol>
}
}
The end result I'm looking for in the HTML is something like:
<h2>Room 1</h2>
<ol>
<li>Bunk 1</li>
<li>Bunk 2</li>
</ol>
<h2>Room 2</h2>
<ol>
<li>Bunk 3</li>
<li>Bunk 4</li>
</ol>
What is the best practice in ASP.NET MVC 4 with EF5 to achieve this kind of result when passing "multidimensional" (is this multidimensional?) data?
Don't rely on ViewData. Store the data that you want to pass on to your view in a proper ViewModel:
public class RoomViewModel
{
List<Room> Rooms { get; set;}
...
}
Store your data in one of those.
Your Controller method then returns an instance of it:
public RoomViewModel GetRooms(int someParameter)
{
RoomViewModel result = new RoomViewModel();
result.Rooms = RoomHelper.Something(someParameter);
...
return result;
}
Your View declares its model on top:
#model MyApplication.ViewModels.RoomViewModel
and hence you use it in your View.
<h2>#Model.Rooms.Count rooms found</h2>
etc.
Use a code block in your view instead of adding an '#' in front of each C# statement:
#{
foreach (var room in ViewBag.Rooms)
{
#Html.Raw("<h2>" + room.RoomName + "</h2>");
if (ViewData["Room_" + room.RoomId].Count() > 0)
{
#Html.Raw("<ol>");
foreach (var bunk in ViewData["Room_" + room.RoomId])
{
#Html.Raw("<li>" + bunk.BunkId + "</li>");
}
#Html.Raw("</ol>");
}
}
}
You should avoid the use of #HtmlRaw("") as far as possible as it has a XSS vulnerability. But this should put you on the right track.
As per description given by you it seems that Bunk is associated with rooms. If that's the case then Bunk may have some id for pointing to room it belongs. Now you can create a ViewModel like this
public class BunkViewModel:Bunk
{
public BunkViewModel()
{
Mapper.CreateMap<Bunk,BunkViewModel>();
}
//I'm assuming that you already have some id in bunk to point to room it belongs.
//But writing it here to make it clear
public int RoomId { get; set; }
public string RoomName { get; set; }
//Use AutoMapper to Map
public static BunkViewModel Map(Bunk source)
{
return Mapper.Map<Bunk,BunkViewModel>(source);
}
}
Now in your controller
public ActionResult ActionName()
{
var result = new List<BunkViewModel>();
var rooms = RoomHelper.FindAllRooms();
var bunks = BunkHelper.GetAllBunks();
foreach(var bunk in bunks)
{
var bunkViewModel = BunkViewModel.Map(bunk);
var room = rooms.FirstorDefault(r=>room.RoomId == bunk.RoomId);
bunkViewModel.RoomId = room.RoomId; //No need to set if you already have this id in bunk
bunkViewModel.RoomName = room.RoomName;
result.Add(bunkViewModel);
}
return View(result.);
}
Now in your view you can do like this
#model List<MyApplication.ViewModels.RoomViewModel>
#foreach(var bunk in Model)
{
//Print it here as you want..
}

Categories

Resources