So I'm trying to create a component that I can reuse to edit an object. Basically, it's a form that can show different inputs based on the type of the object's elements. For the moment I can display my component on another one I have access to the data, but I can't pass this data to another page. So here's what I've done so far:
So this is the component:
#using System.Reflection;
#typeparam TItem
#if (Test != null)
{
#foreach (PropertyInfo info in typeof(TItem).GetProperties())
{
<div class="container">
#info.Name :
#info.GetValue(Test)
<br />
<EditForm Model="Test">
<div >
#if (info.PropertyType == typeof(string))
{
<input class="col " type="text" #onchange="(a) => UpdateObj(a.Value!,info)">
}
</div>
</EditForm>
</div>
}
}
#code {
public object? test { get; set; }
[Parameter]
public TItem? Test { get; set; }
[Parameter]
public Func<TItem, Task>? NewValue { get; set; }
async Task UpdateObj(object value, PropertyInfo type)
{
if (NewValue != null && Test != null)
{
test = value;
await NewValue(Test);
}
StateHasChanged();
}
}
And this is where I used it in the previous example:
#page "/test"
#using Component;
#if( test != null)
{
<Component.ObjectEditor Test="test" TItem="testRecord"
NewValue="(a) => {
json = a.JsonSerializeTo();
StateHasChanged();
return Task.CompletedTask;}"/>
}
#json
#code
{
public string? json = null;
public record testRecord
{
public string? Text { get; set; }
}
public testRecord test = new testRecord() ;
}
Related
I am working to create a custom Blazor multiple select HTML component. It works until I add the validation. Also if I disable multiple select and leave the validation on, it works.
When multiple select with validation is on I get this error:
InvalidOperationException: MultipleSelect requires a value for the 'ValueExpression' parameter. Normally this is provided automatically when using 'bind-Value'.
I haven't been able to use the 'bind-Value' property because I get this error.
The documentation I have been able to find so far only address building a custom component from an HTML <select> element when the multiple select option is not in use.
How do I go about creating a <select> element with multiple select enabled?
Custom multiple select component
MultipleSelect.razor
#using CustomComponents.DataModels
#using System.Linq.Expressions
#using System
#using System.Collections.Generic
#inherits InputBase<string>
<div class="row">
<div class="col-3">
<select id="#Id" #bind=#CurrentValue class="form-control #CssClass" multiple="multiple" size="#BoxHieght" style="width:#BoxWidth">
#foreach (var option in Options)
{
<option #onclick="#(() => SelectOption(option))" value="#option.Value">#option.Text</option>
}
</select>
</div>
</div>
#code {
[Parameter]
public string Id { get; set; }
[Parameter]
public List<Option> Options { get; set; } = new List<Option>();
[Parameter]
public Option SelectedOption { get; set; } = new Option { Text = " ", Value = " " };
[Parameter]
public int BoxHieght { get; set; } = 5;
[Parameter]
public string BoxWidth { get; set; } = "auto";
[Parameter, EditorRequired]
public Expression<Func<string>> ValidationFor { get; set; } = default!;
private void SelectOption(Option option)
{
SelectedOption = option;
}
protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
{
try
{
result = value;
validationErrorMessage = null;
return true;
}
catch (Exception exception)
{
result = null;
validationErrorMessage = exception.Message;
return false;
}
}
}
Option data model object
Option.cs
namespace CustomComponents.DataModels
{
public class Option
{
public string Text { get; set; }
public string Value { get; set; }
}
}
Web Form Model
FormModel.cs
using CustomComponents.Data.DataModels;
namespace BlazorApp2.Pages.PageModels
{
public class FormModel
{
public Option Option { get; set; } = new Option();
}
}
Data Model
State.cs
using System.ComponentModel.DataAnnotations;
namespace BlazorApp2.Data.DataModels
{
public class State
{
[Required]
public string Name { get; set; }
[Required]
public string Abbreviation { get; set; }
}
}
Web Form
Index.razor
#page "/"
#using CustomComponents.Components
#using CustomComponents.Data.DataModels
#using CustomComponents.Pages.PageModels
<PageTitle>Mutiple Select Component</PageTitle>
<EditForm Model="#model" OnValidSubmit="ValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<MyComponent Options="#options" #bind-Value="#model.Option" ValidationFor="() => State.Name"></MyComponent>
Selected option:
<div class="row">
<div class="col">
#model.Option.Value #model.Option.Text
</div>
</div>
<div class="row">
<div class="col">
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</div>
<div class="row">
<div class="col">
#if (formSubmitted) #FormSubmitted
</div>
</div>
<div class="row">
<div class="col">
#if (formSubmitted) #StateSubmitted
</div>
</div>
</EditForm>
#code {
private List<Option> options = new List<Option>();
public FormModel model = new FormModel();
public State State { get; set; } = new State();
private List<State> states = new List<State>
{
new State { Name = "Utah", Abbreviation = "UT" },
new State { Name = "Texas", Abbreviation = "TX" },
new State { Name = "Florida", Abbreviation = "FL" }
};
public string FormSubmitted { get; set; } = "Form submitted.";
public string StateSubmitted { get; set; } = string.Empty;
private bool formSubmitted = false;
protected override void OnInitialized()
{
foreach(State state in states)
{
options.Add(new Option{ Value = state.Abbreviation, Text = state.Name});
}
model.Option = options[0];
}
public void ValidSubmit()
{
State.Abbreviation = model.Option.Value;
State.Name = model.Option.Text;
formSubmitted = true;
StateSubmitted = $"{State.Abbreviation} {State.Name}";
}
}
because you inherit the component InputBase, you must use bind-value.
I edited a lot of the code to make it work
#using BlazorApp2.Client.Components
#using System.Linq.Expressions
#using System
#using System.Collections.Generic
#using System.Diagnostics.CodeAnalysis
#inherits InputBase<Option>
<div class="row">
<div class="col-3">
<select id="#Id" class="form-control" size="#BoxHieght" style="width:#BoxWidth"
#bind="OptionValueSelected" #bind:event="oninput">
#foreach (var option in Options)
{
<option value="#option.Value">#option.Text</option>
}
</select>
<p>Selected option:#SelectedOption.Value </p>
</div>
</div>
#code {
[Parameter]
public string Id { get; set; }
[Parameter]
public List<Option> Options { get; set; } = new List<Option>();
[Parameter]
public Option SelectedOption { get; set; } = new Option { Text = " ", Value = " " };
[Parameter]
public int BoxHieght { get; set; } = 5;
[Parameter]
public string BoxWidth { get; set; } = "auto";
[Parameter, EditorRequired]
public Expression<Func<string>> ValidationFor { get; set; } = default!;
private string OptionValueSelected
{
get => CurrentValue.Value;
set
{
CurrentValue = Options.Find(o => o.Value == value);
}
}
protected override bool TryParseValueFromString(string value,
[MaybeNullWhen(false)] out Option result, [NotNullWhen(false)] out string validationErrorMessage)
{
try
{
result = Options.First(o => o.Value == value.ToString());
validationErrorMessage = null;
return true;
}
catch (Exception exception)
{
result = null;
validationErrorMessage = exception.Message;
return false;
}
}
}
After very long research, Here are some important changes I made:
Inherits from InputBase of type Option not string. Reason: so the form context knows the type and binds correctly
Bind value with a property with setters. Reason: To convert from string to option
Set the value of SelectedOption from the Input base CurrectValue. Reason: To alert the form context about the change so that it updates the main view
I Tested the component using this page on new project:
#page "/"
#using BlazorApp2.Client.Components
<PageTitle>Index</PageTitle>
#code {
List<Option> options = new List<Option>
{
new Option{Text = "Test1", Value = "Test1"},
new Option{Text = "Test2", Value = "Test2"}
};
ExampleModel model;
protected override void OnInitialized()
{
model = new ExampleModel();
}
}
<h1>Hello, world!</h1>
<EditForm Model="#model">
<MyComponent Options="#options" #bind-Value="#model.Option"></MyComponent>
</EditForm>
<p>#model.Option.Text : #model.Option.Value</p>
Example Model:
public class ExampleModel
{
public Option Option { get; set; } = new Option();
}
Resources helped me with my research :
Blazor components
Blazor form components binding
Blazor Custom Binding
I'm trying to build a custom dropdown-typeahead component in Blazor. I want it to be as following:
Instead of the classic <select> tag which on click displays a list, i want it to have an <input> tag where the user can write something and search the list. On each character they write, the dropdown <div> opens below with results depending on the search text. The user can select any of the displaying results of the list. So far so good.
The problem I'm dealing with is that I cannot display the selected item inside the <input> tag. My code is the following:
Parent Component
...
<div class="col" #onfocusout="ClearHstrCommonSearchTextHandler">
<label class="form-label">Location</label>
<TypeaheadDropdownComponent OnEmptySearchText="ClearHstrCommonSearchTextHandler"
SearchMethod="SearchHstrCommon" />
#if (showDropdownResults && HstrCommonDisplayed.Count > 0)
{
<div class="custom-dropdown-results">
#foreach (HstrCommonDTO hstr in HstrCommonDisplayed)
{
<div class="custom-dropdown-results-item" #onclick="() => { locationHstrId = hstr.HstrId; showDropdownResults = false; }">
#hstr.Title <br />
</div>
}
</div>
}
</div>
...
#code {
[Parameter] public List<HstrCommonDTO> HstrCommon { get; set; }
private int locationHstrId = -1;
private bool showDropdownResults = false;
private List<HstrCommonDTO> HstrCommonDisplayed = new();
...
private void SearchHstrCommon(string searchText)
{
searchText = searchText.RemoveDiacritics()
.Trim()
.ToUpper();
showDropdownResults = true;
HstrCommonDisplayed = HstrCommon.FindAll(x => x.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase)).ToList();
}
private void ClearHstrCommonSearchTextHandler()
{
showDropdownResults = false;
HstrCommonDisplayed = new();
locationHstrId = -1;
}
}
TypeaheadDropdownComponent
<div style="position: relative">
<input class="form-control"
type="text"
autocomplete="off"
placeholder="#Placeholder"
#bind-value="#SearchText"
#bind-value:event="oninput" />
</div>
#code {
[Parameter] public string Placeholder { get; set; } = "Search";
[Parameter] public int MinimumLength { get; set; } = 1;
[Parameter] public int Debounce { get; set; } = 200;
[Parameter] public EventCallback<string> SearchMethod { get; set; }
[Parameter] public EventCallback OnEmptySearchText { get; set; }
private Timer _debounceTimer;
private bool _isSearching = false;
private string _searchText = string.Empty;
private string SearchText
{
get => _searchText;
set
{
_searchText = value;
if (value.Length == 0)
{
_debounceTimer.Stop();
OnEmptySearchText.InvokeAsync();
}
else if (value.Length >= MinimumLength)
{
_debounceTimer.Stop();
_debounceTimer.Start();
}
}
}
protected override void OnInitialized()
{
_debounceTimer = new Timer();
_debounceTimer.Interval = Debounce;
_debounceTimer.AutoReset = false;
_debounceTimer.Elapsed += Search;
base.OnInitialized();
}
protected async void Search(Object source, ElapsedEventArgs e)
{
_isSearching = true;
await InvokeAsync(StateHasChanged);
await SearchMethod.InvokeAsync(_searchText);
_isSearching = false;
await InvokeAsync(StateHasChanged);
}
}
With my code, I take the required value (locationHstrId) as expected, but in the <input> tag of the TypeaheadDropdownComponent, the text which the user wrote for searching is displayed.
I cannot figure out what I have to add to my code in order the selected word of the dropdown list to be displayed in the input tag.
Thanks in advance for your time!
Even though the design of the component is 'strange' (I don't understand why the results are not in the same component?), I will provide you with a solution to your problem.
Add a public function to your TypeaheadDropdownComponent that sets the search value.
public void SetSearchValue(string value){
_searchText = value;
StateHasChanged();
}
Now, in the parent component, get the dropdown component's reference and call the SetSearchValue when selecting an item.
...
<div class="col" #onfocusout="ClearHstrCommonSearchTextHandler">
<label class="form-label">Location</label>
<TypeaheadDropdownComponent #ref="TypeaheadRef" OnEmptySearchText="ClearHstrCommonSearchTextHandler"
SearchMethod="SearchHstrCommon" />
#if (showDropdownResults && HstrCommonDisplayed.Count > 0)
{
<div class="custom-dropdown-results">
#foreach (HstrCommonDTO hstr in HstrCommonDisplayed)
{
<div class="custom-dropdown-results-item" #onclick="() => OnItemSelected(hstr)">
#hstr.Title <br />
</div>
}
</div>
}
</div>
...
#code{
TypeheadDropdownComponent TypeaheadRef;
public void OnItemSelected(HstrCommonDTO hstr){
locationHstrId = hstr.HstrId;
showDropdownResults = false;
TypeaheadRef.SetSearchValue("<your value>");
}
}
Getting null exception trying to add product in to cart
In the code portion below, when I run the project, it seems to work normally, but when I try to add a product to the cart, it throws the exception. I'm getting really stressed out ... I'm about to leave this.
#model Produto
#{
<div class="card text-right" style="width: 20rem;">
<div class="card-body">
<h4 class="card-title">#Model.Nome</h4>
<h3 class="card-title">#Model.Preco.ToString("c")</h3>
</div>
<form id="#Model.ProdutoID" asp-action="AddPCarrinho"
asp-controller="Carrinho" method="post">
<input type="hidden" asp-for="ProdutoID" />
<input type="hidden" name="returnUrl"
value="#ViewContext.HttpContext.Request.PathAndQuery()" />
<span class="card-text p-1">
#Model.Descricao
<button type="submit" class="btn btn-success btn-sm pull-right" style="float:right">
Add to Cart
</button>
</span>
</form>
</div>
}
This is the controller
public class CarrinhoController : Controller
{
private IProdutoRepositorio repositorio;
public CarrinhoController(IProdutoRepositorio repo)
{
repositorio = repo;
}
public ViewResult Index(string returnUrl)
{
return View(new CarrinhoIndexViewModel
{
Carrinho = GetCarrinho(),
ReturnUrl = returnUrl
});
}
public RedirectToActionResult AddPCarrinho(Guid produtId, string returnUrl)
{
Produto produto = repositorio.All
.FirstOrDefault(p => p.ProdutoID == produtId);
if (produtId != null)
{
Carrinho carrinho = GetCarrinho();
carrinho.AddItem(produto, 1);
SalvarCarrinho(carrinho);
}
return RedirectToAction("Index", new { returnUrl });
}
}
This is the ViewModel
namespace SportsStore.Models.ViewModels{
public class CarrinhoIndexViewModel{
public Carrinho Carrinho { get; set; }
public string ReturnUrl { get; set; }
}
}
This is the class where i'm getting the exception null reference
public class Carrinho{
private List<CartLine> lineCollection = new List<CartLine>();
public virtual void AddItem(Produto produto, int quantidade){
CartLine line = lineCollection
.Where(p => p.Produto.ProdutoID == produto.ProdutoID)
.FirstOrDefault();
if (line == null)
{
lineCollection.Add(new CartLine
{
Produto = produto,
Quantidade = quantidade
});
}
else
{
line.Quantidade += quantidade;
}
}
json.net class
public static class SessionExtensions
{
public static void SetJson(this ISession sessao, string key, object value)
{
sessao.SetString(key, JsonConvert.SerializeObject(value));
}
public static T GetJson<T>(this ISession sessao, string key)
{
var sessaoData = sessao.GetString(key);
return sessaoData == null
? default(T) : JsonConvert.DeserializeObject<T>(sessaoData);
}
}
In the controller, change the line
if (produtId != null)
to
if (produto != null)
As It stands, you are not checking that you are getting something from the repository.
I have a MVC 5 web site search criteria , when my page first loads, the URL looks like this: http://myapp.com.
However, I'd like it to look like this: http://myapp.com?HotelID=12&Rate='abc'&Culture='En' and so on (?HotelID=12&Rate='abc'&Culture='En' is the important part).
also i have class session-helper has the default values that i want to put it inside query string on page load i don't know if that possible .
this is the session helper-class :
public class SessionHelper
{
public SessionHelper()
{
// ------ Set default values here
Rate = "";
HotelID = 0;
CSS_FileName = "default.css";
Culture = "En";
Checkin = DateTime.Today.Date;
Checkout = DateTime.Today.Date.AddDays(1);
//Maximum numbers
MaximumNumberOfRooms = 4;
MaximumNumberOfAdultsPerRoom = 4;
MaximumNumberOfChildrenPerRoom = 3;
MaximumDaysAheadBookable = 450;
MaximumDaysBetweenCheckinCheckout = 31;
// You don't need to create an instance on the first page, this will be created on the class constuctor
IBE_DLL_Instance = new RHN_IBE.IBE();
}
#region String properties
public string Rate
{
get { return HttpContext.Current.Session["Rate"] as string; }
set { HttpContext.Current.Session["Rate"] = value; }
}
public string CSS_FileName
{
get { return HttpContext.Current.Session["CSS_FileName"] as string; }
set { HttpContext.Current.Session["CSS_FileName"] = value; }
}
public string Culture
{
get { return HttpContext.Current.Session["Culture"] as string; }
set { HttpContext.Current.Session["Culture"] = value; }
}
#endregion
#region Integer properties
public int HotelID
{
get { return (int)(HttpContext.Current.Session["HotelID"]); }
set { HttpContext.Current.Session["HotelID"] = value; }
}
public int SearchCriteria_NumberOfAdultsPerRoom
{
get { return (int)(HttpContext.Current.Session["SearchCriteria_NumberOfAdultsPerRoom"]); }
set { HttpContext.Current.Session["SearchCriteria_NumberOfAdultsPerRoom"] = value; }
}
public int SearchCriteria_NumberOfChildrenPerRoom
{
get { return (int)(HttpContext.Current.Session["SearchCriteria_NumberOfChildrenPerRoom"]); }
set { HttpContext.Current.Session["SearchCriteria_NumberOfChildrenPerRoom"] = value; }
}
public int SearchCriteria_NumberOfRooms
{
get { return (int)(HttpContext.Current.Session["SearchCriteria_NumberOfRooms"]); }
set { HttpContext.Current.Session["SearchCriteria_NumberOfRooms"] = value; }
}
//Maximum numbers defaulot value
public int MaximumNumberOfRooms
{
get { return (int)(HttpContext.Current.Session["MaximumNumberOfRooms"]); }
set { HttpContext.Current.Session["MaximumNumberOfRooms"] = value; }
}
public int MaximumNumberOfAdultsPerRoom
{
get { return (int)(HttpContext.Current.Session["MaximumNumberOfAdultsPerRoom"]); }
set { HttpContext.Current.Session["MaximumNumberOfAdultsPerRoom"] = value; }
}
public int MaximumNumberOfChildrenPerRoom
{
get { return (int)(HttpContext.Current.Session["MaximumNumberOfChildrenPerRoom"]); }
set { HttpContext.Current.Session["MaximumNumberOfChildrenPerRoom"] = value; }
}
public int MaximumDaysAheadBookable
{
get { return (int)(HttpContext.Current.Session["MaximumDaysAheadBookable"]); }
set { HttpContext.Current.Session["MaximumDaysAheadBookable"] = value; }
}
public int MaximumDaysBetweenCheckinCheckout
{
get { return (int)(HttpContext.Current.Session["MaximumDaysBetweenCheckinCheckout"]); }
set { HttpContext.Current.Session["MaximumDaysBetweenCheckinCheckout"] = value; }
}
#endregion
#region Date properties
public DateTime Checkin
{
get { return (DateTime)(HttpContext.Current.Session["Checkin"]); }
set { HttpContext.Current.Session["Checkin"] = value; }
}
public DateTime Checkout
{
get { return (DateTime)(HttpContext.Current.Session["Checkout"]); }
set { HttpContext.Current.Session["Checkout"] = value; }
}
#endregion
#region DLL Instance
public RHN_IBE.IBE IBE_DLL_Instance
{
get { return (RHN_IBE.IBE)(HttpContext.Current.Session["IBE_DLL_Instance"]); }
set { HttpContext.Current.Session["IBE_DLL_Instance"] = value; }
}
#endregion
}
This is my controller i initialize session-helper class as global variable to call it once the page load and sit the default values on page-load
public class HomeController : Controller
{
SessionHelper mysession = new SessionHelper();
[HttpGet]
[ActionName("Index")]
public ActionResult Index_Get()
{
//checkURL();
pageload();
return View(mysession);
}
}
And this is my form :
#using (Html.BeginForm("Index", "Home", FormMethod.Get))
{
<div class="row">
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-2">
<p class="font">#Resource.criteria_Arrival</p>
<input class="calendarr font" name="Arrival" readonly type="text" id="Arrival" onchange="changedDate()">
</div>
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-2">
<p class="font">#Resource.criteria_Departure</p>
<input class="calendarr font" name="Departure" readonly type="text" id="Departure">
</div>
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-2 font">
<p>#Resource.criteria_Rooms</p>
<select id="Rooms" name="Rooms" class="dropdown">
#for (int i = 1; i <= Model.MaximumNumberOfRooms; i++)
{
<option value="#i">#i</option>
}
</select>
</div>
<div class="clearfix visible-md-block">
</div>
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-2 font">
<p>#Resource.criteria_Persons</p>
<select id="Persons" name="Persons" class="dropdown">
#for (int j = 1; j <= Model.MaximumNumberOfAdultsPerRoom; j++)
{
<option value="#j">#j</option>
}
</select>
</div>
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-1 font">
<p>#Resource.criteria_Children</p>
<select id="Childrens" name="Childrens" class="dropdown">
#for (int c = 1; c <= Model.MaximumNumberOfChildrenPerRoom; c++)
{
<option value="#c">#c</option>
}
</select>
</div>
<div class="col-xs-6 col-sm-6 col-md-4 col-lg-3 font">
<br />
<input type="submit" class="btns" value=#Resource.criteria_btn_Search /><br />
#Resource.criteria_link_AlreadyamemberLogin
</div>
<div class="clearfix visible-lg-block">
</div>
</div>
}
I tried several ways but not worked with me any advice . thanks
I am not sure why you would need a session if you want to store the information in the url. In my answer I am not using the session, but instead focus on the url.
First of all you can use an object to automatically bind the parameters:
//using System.ComponentModel.DataAnnotations;
public class SessionParameters
{
[Required]
public string Rate { get; set; } = "";
[Required]
public string Culture { get; set; } = "En";
[Required]
public int HotelID { get; set; } = 0;
public int SearchCriteria_NumberOfAdultsPerRoom { get; set; }
public int SearchCriteria_NumberOfChildrenPerRoom { get; set; }
public int SearchCriteria_NumberOfRooms { get; set; }
public int MaximumNumberOfRooms { get; set; } = 4;
public int MaximumNumberOfAdultsPerRoom { get; set; } = 4;
public int MaximumNumberOfChildrenPerRoom { get; set; } = 3;
public int MaximumDaysAheadBookable { get; set; } = 450;
public int MaximumDaysBetweenCheckinCheckout { get; set; } = 31;
// You may want to use a specific format for dates.
public DateTime Checkin { get; set; } = DateTime.Today.Date;
public DateTime Checkout { get; set; } = DateTime.Today.Date.AddDays(1);
}
Notice the Required fields. In your controller add the class as parameter:
public class HomeController : Controller
{
SessionHelper mysession = new SessionHelper();
[HttpGet]
[ActionName("Index")]
public ActionResult Index_Get(SessionParameters pars)
{
if (!ModelState.IsValid)
return RedirectToAction("Index", routeValues: new SessionParameters());
//checkURL();
pageload();
return View(pars);
}
}
How this works: pars will be initialized as a new SessionParameters object (meaning that the default values are set!) where present url parameters are overwritten with the actual value.
We cannot tell whether this is a first entry, but we can test the ModelState. If invalid, we need to set the default values.
What will work in this case, is the Rate field. Rate is required but has a default value of "". This will cause ModelState to be invalid if Rate is omitted.
So in case you hit http://myapp.com, pars is automatically mapped as a new SessionParameters object. And because Rate == "" ModelState wil not be valid. Now we can redirect to the same action with the new SessionParameters (containing the default values) which are now shown as parameters in the url.
It may not be exactly what you are looking for, but I hope this gives you an idea on how you may solve this issue.
I'm drawing a blank on this. I have two foreach loops that are creating div's and I'm trying to get 1 or two items from the 2nd foreach to display every 2 items from the first foreach.... here's the code..
#foreach (var stream in Model.LiveStreams)
{
<div id="divLiveStream" class="well-livestream well-sm">
<div class="row">
<div class="col-md-2">
<div class="ui-icon-image">
<img src="~/Images/placeholder.jpg" width="100%" />
</div>
</div>
<div class="col-md-10">
<div class="text-left row">
<div class="col-md-12">
#stream.UserName
</div>
<div class="col-md-12">
#stream.Status
</div>
</div>
</div>
</div>
</div>
}
#foreach (var post in Model.LivePosts)
{
<div id="divLivePost" class="well-liveposts well-sm">
<div class="row">
<div class="col-md-2">
<div class="ui-icon-image">
<img src="~/Images/placeholder.jpg" width="100%" />
</div>
</div>
<div class="col-md-10">
<div class="text-left row">
<div class="col-md-12">
#post.AspNetUser.UserName
</div>
<div class="col-md-12">
#post.PostDescription
</div>
</div>
</div>
</div>
</div>
}
So for further clarification, I'm trying to display 2 divLiveStreams, then display 2 divLivePost, and continue on with that pattern. I'm new to MVC so I'm not sure how to go about this and I'm drawing all kinds of blanks on this Friday.. Thanks in advance!
Edit: Here is my ViewModel.
public class LiveStreamViewModel
{
public IEnumerable<LiveStream> LiveStreams { get; set; }
public IEnumerable<tPost> LivePosts { get; set; }
}
Edit: Probably a dumb question, but I use my controller to return the model. Before I had my controller like below, how am I suppose to call the new ViewModel that you created in the controller?
Here is my Controller.
public ActionResult LiveStream()
{
Service service = new Service();
LiveStreamViewModel model = new LiveStreamViewModel
{
LiveStreams = service.GetLiveStream(),
LivePosts = service.GetLivePosts()
};
if (TempData["Message"] != null) ViewBag.Message = TempData["Message"]; ViewBag.Header = "Success!";
return View(model);
}
}
And my data service.
public IEnumerable<LiveStream> GetLiveStream()
{
using (dboMyJeepTraderEntities context = new dboMyJeepTraderEntities())
{
return (from s in context.tStatusUpdates select new LiveStream
{
Status = s.Status,
UserName = s.AspNetUser.UserName
}).ToList();
}
}
public IEnumerable<tPost> GetLivePosts()
{
return (from p in _context.tPosts select p).ToList();
}
You need one viewmodel, with the alternation happening with the hydration of the viewmodel.
public abstract class AbstractThing
{
public abstract string TheUserName { get; }
public abstract string TheDescription { get; }
}
public class LivePost : AbstractThing
{
public string UserName { get; set; }
public string PostDescription { get; set; }
public override string TheUserName
{
get { return this.UserName; }
}
public override string TheDescription
{
get { return this.PostDescription; }
}
}
public class LiveStream : AbstractThing
{
public string UserName { get; set; }
public string Status { get; set; }
public override string TheUserName
{
get { return this.UserName; }
}
public override string TheDescription
{
get { return this.Status; }
}
}
public class AlternatingThingsViewModel
{
public ICollection<AbstractThing> AbstractThings { get; private set; }
public AlternatingThingsViewModel(ICollection<LivePost> lps, ICollection<LiveStream> lss)
{
this.AbstractThings = new List<AbstractThing>();
/* this is not correct...here is where you would "two by two" add items to the this.AbstractThings in the order you want. the key is to have one collection exposed by the viewmodel */
if (null != lps)
{
foreach (AbstractThing at in lps)
{
this.AbstractThings.Add(at);
}
}
if (null != lss)
{
foreach (AbstractThing at in lss)
{
this.AbstractThings.Add(at);
}
}
}
}
Now you loop on the single collection
#foreach (var post in Model.AbstractThings)
{
<div id="divLivePost" class="well-liveposts well-sm">
<div class="row">
<div class="col-md-2">
<div class="ui-icon-image">
<img src="~/Images/placeholder.jpg" width="100%" />
</div>
</div>
<div class="col-md-10">
<div class="text-left row">
<div class="col-md-12">
#post.TheUserName
</div>
<div class="col-md-12">
#post.TheDescription
</div>
</div>
</div>
</div>
</div>
}
Below is a "one then other one" implementation...as long as the LivePost count is greater than the LiveStream. And no error checking. But gives you an idea.
public class AlternatingThingsViewModel
{
public ICollection<AbstractThing> AbstractThings { get; private set; }
public AlternatingThingsViewModel(ICollection<LivePost> lps, ICollection<LiveStream> lss)
{
this.AbstractThings = new List<AbstractThing>();
/* this is a "one by one" with no error checking if the counts are different */
IEnumerator<AbstractThing> livePostEnum = null == lps ? null : lps.GetEnumerator();
IEnumerator<AbstractThing> liveStreamEnum = null == lss ? null : lss.GetEnumerator();
if (null != liveStreamEnum)
{
liveStreamEnum.MoveNext();
}
if (null != livePostEnum)
{
while (livePostEnum.MoveNext() == true)
{
AbstractThing lpCurrent = livePostEnum.Current;
AbstractThing lsCurrent = null == liveStreamEnum ? null : liveStreamEnum.Current;
if (null != liveStreamEnum)
{
liveStreamEnum.MoveNext();
}
this.AbstractThings.Add(lpCurrent);
if (null != lsCurrent)
{
this.AbstractThings.Add(lsCurrent);
}
}
}
}
}
and some calling code.
static void Main(string[] args)
{
try
{
ICollection<LivePost> lps = new List<LivePost>();
lps.Add(new LivePost() { UserName = "LivePostUserName1", PostDescription = "LivePostDescription1" });
lps.Add(new LivePost() { UserName = "LivePostUserName2", PostDescription = "LivePostDescription2" });
lps.Add(new LivePost() { UserName = "LivePostUserName3", PostDescription = "LivePostDescription3" });
lps.Add(new LivePost() { UserName = "LivePostUserName4", PostDescription = "LivePostDescription4" });
ICollection<LiveStream> lss = new List<LiveStream>();
lss.Add(new LiveStream() { UserName = "LiveStreamUserName1", Status = "LiveStreamStatus1" });
lss.Add(new LiveStream() { UserName = "LiveStreamUserName2", Status = "LiveStreamStatus2" });
//lss.Add(new LiveStream() { UserName = "LiveStreamUserName3", Status = "LiveStreamStatus3" });
AlternatingThingsViewModel atmv = new AlternatingThingsViewModel(lps, lss);
int modCheckCount = 0;
foreach (AbstractThing at in atmv.AbstractThings)
{
Console.WriteLine("{0}, {1}", at.TheUserName, at.TheDescription);
if (++modCheckCount % 2 == 0)
{
Console.WriteLine("");
}
}
Well, so - yeah - doing it in the Controller obviously makes sense. But, then again, Razor is just (mostly) C# so whatever you can do in one you can do in the other:
#{
var streamIndex = 0;
var postIndex = 0;
}
#while (streamIndex < Model.LiveStreams.Count() || postIndex < Model.LivePosts.Count()) {
#foreach (var stream in Model.LiveStreams.Skip(streamIndex).Take(2)) {
// html code
streamIndex++;
}
#foreach (var post in Model.LivePosts.Skip(postIndex).Take(2)) {
// html code
postIndex++;
}
}