Blazor - Component wrapping and data binding - c#

I've made a range input in a blazor component.
And now i'm trying to make a logarithme range input.
To do this, I wanted to use my first range input, and simply make a wrapper between a fake value in the range input and the real value.
but I think I havent well understood the component binding, the index page is not notified of modifications.
Here is the range input component:
<div id="rc-#ID">
#(new MarkupString($#"<style>
#rc-{ID} {{
position: relative;
width: 100%;
}}
#rc-{ID} > input[type='range'] {{
padding: 0;
margin: 0;
display: inline-block;
vertical-align: top;
width: 100%;
--range-color: hsl(211deg 100% 50%);
background: var(--track-background);
}}
#rc-{ID} > input[type='range']::-moz-range-track {{
border-color: transparent; /* needed to switch FF to 'styleable' control */
}}
#rc-{ID} > input[name='low-range'] {{
position: absolute;
}}
#rc-{ID} > input[name='low-range']::-webkit-slider-thumb {{
position: relative;
z-index: 2;
}}
#rc-{ID} > input[name='low-range']::-moz-range-thumb {{
transform: scale(1); /* FF doesn't apply position it seems */
z-index: 1;
}}
#rc-{ID} > input[name='high-range'] {{
position: relative;
--track-background: linear-gradient(to right, transparent {(int)(100 * (LowerValue - MinBound) / (MaxBound - MinBound) + 1 + 0.5f)}%, var(--range-color) 0, var(--range-color) {(int)(100 * (HigherValue - MinBound) / (MaxBound - MinBound) - 1 + 0.5f)}%, transparent 0 ) no-repeat 0 50% / 100% 100%;
background: linear-gradient(to right, gray {(int)(100 * (LowerValue - MinBound) / (MaxBound - MinBound) + 1+ 0.5f)}%, transparent 0, transparent {(int)(100 * (HigherValue - MinBound) / (MaxBound - MinBound) - 1 + 0.5f)}%, gray 0 ) no-repeat 0 50% / 100% 30%
}}
#rc-{ID} > input[type='range']::-webkit-slider-runnable-track {{
background: var(--track-background);
}}
#rc-{ID} > input[type='range']::-moz-range-track {{
background: var(--track-background);
}}
</style>"))
<input class="custom-range" name="low-range" type="range" min="#MinBound" max="#MaxBound" step="#Step" #bind="#LowerValue" #bind:event="oninput" />
<input class="custom-range" name="high-range" type="range" min="#MinBound" max="#MaxBound" step="#Step" #bind="#HigherValue" #bind:event="oninput" />
</div>
#code
{
[Parameter] public float MinBound { get; set; } = 0;
[Parameter] public float MaxBound { get; set; } = 1;
[Parameter] public float Step { get; set; } = 0.01f;
[Parameter]
public float? ValueLow
{
get
{
var res = Math.Min(_valueLow, _valueHigh);
if (res == MinBound)
return null;
return res;
}
set
{
if (!value.HasValue)
{
if (_valueLow.Equals(MinBound))
return;
_valueLow = MinBound;
}
else
{
if (_valueLow.Equals(value.Value))
return;
_valueLow = value.Value;
}
if (_valueLow > _valueHigh)
{
_valueLow = _valueHigh;
_valueHigh = value.Value;
ValueHighChanged.InvokeAsync(_valueHigh);
}
if (_valueLow == MinBound)
ValueLowChanged.InvokeAsync(null);
else
ValueLowChanged.InvokeAsync(_valueLow);
}
}
[Parameter]
public float? ValueHigh
{
get
{
var res = Math.Max(_valueLow, _valueHigh);
if (res == MaxBound)
return null;
return res;
}
set
{
if (!value.HasValue)
{
if (_valueHigh.Equals(MaxBound))
return;
_valueHigh = MaxBound;
}
else
{
if (_valueHigh.Equals(value.Value))
return;
_valueHigh = value.Value;
}
if (_valueLow > _valueHigh)
{
_valueHigh = _valueLow;
_valueLow = value.Value;
ValueLowChanged.InvokeAsync(_valueLow);
}
if (_valueHigh == MaxBound)
ValueHighChanged.InvokeAsync(null);
else
ValueHighChanged.InvokeAsync(_valueHigh);
}
}
[Parameter] public EventCallback<float?> ValueLowChanged { get; set; }
[Parameter] public EventCallback<float?> ValueHighChanged { get; set; }
float _valueLow = 0;
float _valueHigh = 1;
private float LowerValue
{
get => Math.Min(_valueLow, _valueHigh);
set => ValueLow = value;
}
private float HigherValue
{
get => Math.Max(_valueLow, _valueHigh);
set => ValueHigh = value;
}
string ID = Guid.NewGuid().ToString().Replace("-", "").Substring(15);
}
And here is my Range input log component:
<RangeControl #bind-ValueLow="Low"
#bind-ValueHigh="High"
MaxBound="max"
MinBound="min"
Step="1" />
<div class="d-flex">
<strong>Log values : </strong>
<span>#Low</span>
<span class="ml-2">#High</span>
</div>
#code
{
private float min = 1.0f;
private float max = 100.0f;
[Parameter] public float MinBound { get; set; } = 10;
[Parameter] public float MaxBound { get; set; } = 10000;
[Parameter] public float Step { get; set; } = 1;
private float r => MinBound == 0 ? MaxBound : (MaxBound / MinBound);
private float? _valueLow;
[Parameter]
public float? ValueLow
{
get => _valueLow;
set
{
if (value == _valueLow) return;
_valueLow = value;
ValueLowChanged.InvokeAsync(ValueLow);
}
}
private float? _valueHigh;
[Parameter]
public float? ValueHigh
{
get => _valueHigh;
set
{
if (value == _valueHigh) return;
_valueHigh = value;
ValueHighChanged.InvokeAsync(ValueHigh);
}
}
private float? Low
{
get
{
if (ValueLow.HasValue)
return (float)((min = max) * Math.Log(ValueLow.Value) / Math.Log(r));
return null;
}
set
{
if (value.HasValue)
ValueLow = (float)Math.Exp(value.Value * Math.Log(r) / (max - min));
else
ValueLow = null;
}
}
private float? High
{
get
{
if (ValueHigh.HasValue)
return (float)((min = max) * Math.Log(ValueHigh.Value) / Math.Log(r));
return null;
}
set
{
if (value.HasValue)
ValueHigh = (float)Math.Exp(value.Value * Math.Log(r) / (max - min));
else
ValueHigh = null;
}
}
[Parameter] public EventCallback<float?> ValueLowChanged { get; set; }
[Parameter] public EventCallback<float?> ValueHighChanged { get; set; }
}
And the index page :
#page "/"
<h1>Hello, world!</h1>
<RangeControl #bind-ValueHigh="ValueHigh" #bind-ValueLow="ValueLow" MinBound="10" MaxBound="10000" Step="1"></RangeControl>
<br />
<RangeControlLog #bind-ValueHigh="ValueHigh" #bind-ValueLow="ValueLow" MinBound="10" MaxBound="10000" Step="1"></RangeControlLog>
<div class="d-flex">
<strong>Real values : </strong>
<span>#ValueLow</span>
<span class="ml-2">#ValueHigh</span>
</div>
#code {
float? ValueHigh = null;
float? ValueLow = null;
}

You cannot have nested #bind- i.e. have a wrapper that uses #bind- of the wrapped component and also expose a property to be used with #bind-.
You need to pass Foo and FooChanged to the component being wrapped.
This means that in your RangeControlLog, you need to pass to RangeControl ValueLow and ValueLowChanged instead of using #bind-ValueLow
<RangeControl ValueLow="Low"
ValueHigh="High"
ValueLowChanged="ValueLowChanged"
ValueHighChanged="ValueHighChanged"
MaxBound="max"
MinBound="min"
Step="1" />
To learn more, you can take a look at the docs about chained binding and also take a look at this question I have made to understand better about ValueChanged and how it works.
But for short, when you use #bind-Foo="Bar" it transforms it into Foo="Bar", FooChanged="#(foo => Bar = foo;)" which are kind of a default value for updating the properties. But it doesn't work when you have multiple #bind-, so you need to pass that directly.
For me, #bind- looks like a syntax sugar for binding properties and when you have the parameters Foo and FooChanged, you can use #bind-Foo.

Related

how to create a generic treeview component in blazor?

could you help me? I want to make a generic tree view component in blazor webassembly but I am a bit lost in how to do it, I want to be able to pass any type of object list to the component, for the moment I have done something very simple, with an object called directory load the component but I would like to replace it with Titem to be able to send any type of list
index.razor
#page "/index"
<h1>Treeview</h1>
<Treeview Directorios="directorios"></Treeview>
#code {
public Directorio[] directorios;
protected async override Task OnInitializedAsync()
{
var fall2014 = new Directorio("Fall 2014", new Directorio[] { }, new string[] { "image1.jpg", "image2.jpg", "image3.jpg" });
var summer2014 = new Directorio("Summer 2014", new Directorio[] { }, new string[] { "image10.jpg", "image20.jpg", "image30.jpg" });
var pictures = new Directorio("Pictures", new Directorio[] { fall2014, summer2014 }, new string[] { });
var music = new Directorio("Music", new Directorio[] { }, new string[] { "song1.mp3", "song2.mp3" });
directorios = new Directorio[] { pictures, music };
}
}
component.razor
<ul>
#foreach (var dir in Directorios)
{
<li>
<span #onclick="#dir.toggle">#dir.getIcon()</span>
<span>#dir.nombre</span>
#if (dir.expanded)
{
<div>
<ul>
#foreach (var archivo in dir.archivos)
{
<li>#archivo</li>
}
</ul>
<Treeview Directorios="#dir.directorios"></Treeview>
</div>
}
</li>
}
</ul>
#code {
[Parameter] public Directorio[] Directorios { get; set; }
}
directory.cs
public class Directorio
{
public bool expanded = false;
public string nombre;
public string[] archivos;
public Directorio[] directorios;
public Directorio(string nombre, Directorio[] directorios, string[] archivos)
{
this.nombre = nombre;
this.directorios = directorios;
this.archivos = archivos;
}
public void toggle()
{
expanded = !expanded;
}
public string getIcon()
{
if (expanded)
{
return "-";
}
return "+";
}
}
Try this one.
In future perhaps I will make a combotree and share with you here.
if you are able to do same, do not hesitate to post it here.
1.Treeview.razor
#typeparam Tvalue
#inherits TreeviewBase<Tvalue>
<ul class="parentUl">
#if (AllItems != null)
{
#foreach (var Pitem in AllItems)
{
if (GetPropertyValue(Pitem, ParentId) == ""|| Convert.ToInt32(GetPropertyValue(Pitem, ParentId)) == 0)
{
if (Convert.ToBoolean(GetPropertyValue(Pitem, HasChildren)))
{
<li>
<span #onclick="#(()=>SpanToggle(Pitem))" class="#_caretcss[Convert.ToInt32(#GetPropertyValue(Pitem, Id))]">#GetPropertyValue(Pitem, Text)</span>
<ul class="#_nestedcss[Convert.ToInt32(#GetPropertyValue(Pitem, Id))]">
#foreach (var Citem in AllItems)
{
if (GetPropertyValue(Pitem, Id) == GetPropertyValue(Citem, ParentId))
{
if (Convert.ToBoolean(GetPropertyValue(Citem, HasChildren)))
{
<li>
<span #onclick="#(()=>SpanToggle(Citem))" class="#_caretcss[Convert.ToInt32(#GetPropertyValue(Citem, Id))]">#GetPropertyValue(Citem, Text)</span>
<ul class="#_nestedcss[Convert.ToInt32(#GetPropertyValue(Citem, Id))]">
#foreach (var C1item in AllItems)
{
if (GetPropertyValue(Citem, Id) == GetPropertyValue(C1item, ParentId))
{
if (Convert.ToBoolean(GetPropertyValue(C1item, HasChildren)))
{
<li>
<span #onclick="#(()=>SpanToggle(C1item))" class="#_caretcss[Convert.ToInt32(#GetPropertyValue(C1item, Id))]">#GetPropertyValue(C1item, Text)</span>
<ul class="#_nestedcss[Convert.ToInt32(#GetPropertyValue(C1item, Id))]">
#foreach (var C2item in AllItems)
{
if (GetPropertyValue(C1item, Id) == GetPropertyValue(C2item, ParentId))
{
if (Convert.ToBoolean(GetPropertyValue(C2item, HasChildren)))
{
<li>
<span #onclick="#(()=>SpanToggle(C2item))" class="#_caretcss[Convert.ToInt32(#GetPropertyValue(C2item, Id))]">#GetPropertyValue(C1item, Text)</span>
</li>
}
else
{
<li>#GetPropertyValue(C2item, Text)</li>
}
}
}
</ul>
</li>
}
else
{
<li>#GetPropertyValue(C1item, Text)</li>
}
}
}
</ul>
</li>
}
else
{
<li>#GetPropertyValue(Citem, Text)</li>
}
}
}
</ul>
</li>
}
else
{
<li>#GetPropertyValue(Pitem, Text)</li>
}
}
}
}
</ul>
2.style.css
<style type="text/css">
/*css reference W3schools. "with small modification."*/
/* css begin*/
.parentUl li ul {
border-left: dashed 2px black;
height: fit-content;
border-start-end-radius: 2px;
}
ul, .parentUl {
list-style-type: none;
}
.parentUl ul li {
position: relative;
}
.parentUl ul li:before {
content: "";
position: absolute;
top: 13px;
left: -40px;
width: 40px;
height: 1px;
border-bottom: dashed 2px black;
}
.parentUl {
margin: 0;
padding: 0;
}
.caret {
cursor: pointer;
-webkit-user-select: none; /* Safari 3.1+ */
-moz-user-select: none; /* Firefox 2+ */
-ms-user-select: none; /* IE 10+ */
user-select: none;
}
.caret::before {
content: "\25B6";
color: black;
display: inline-block;
margin-right: 6px;
transition: all 0.45s;
}
.caret-down::before {
-ms-transform: rotate(60deg); /* IE 9 */
-webkit-transform: rotate(60deg); /* Safari */
transform: rotate(60deg);
transition: all 0.45s;
}
.nested {
display: none;
transition: all 0.45s;
}
.active {
display: block;
transition: all 0.45s;
}
/*css end*/
</style>
3.TreeviewBase.cs
public partial class TreeviewBase<Tvalue>:ComponentBase
{
[Parameter]
public List<Tvalue> DataSource { get; set; }
[Parameter]
public string Id { get; set; }
[Parameter]
public string ParentId { get; set; }
[Parameter]
public string HasChildren { get; set; }
[Parameter]
public string Text { get; set; }
[Parameter]
public string Expanded { get; set; }
protected List<Tvalue> AllItems;
protected Dictionary<int, bool> _caretDown= new Dictionary<int, bool>();
protected Dictionary<int, string> _caretcss=new Dictionary<int,string>();
protected Dictionary<int, string> _nestedcss=new Dictionary<int,string>();
protected override Task OnInitializedAsync()
{
//asigning to its new instance to avoid exceptions.
AllItems = new List<Tvalue>();
AllItems = DataSource.ToArray().ToList();
if (AllItems != null)
{
foreach (var item in AllItems)
{
var _id = Convert.ToInt32(GetPropertyValue(item, Id));
//initializing fields with default value.
_caretDown.Add(_id, true);
_caretcss.Add(_id, "caret");
_nestedcss.Add(_id, "nested");
}
}
return base.OnInitializedAsync();
}
protected override Task OnParametersSetAsync()
{
//This will check if the first item in the
// list/collection has a "parentId" then remove the "parentId" from it.
//Because we use the first item as a reference in the razor file, so it must not have "parentId".
var Parem = AllItems.First();
switch (GetPropertyType(Parem, ParentId))
{
case "Int32":
if (Convert.ToInt32(GetPropertyValue(Parem, ParentId)) > 0)
{
SetPropertyValue<int>(Parem, ParentId, 0);
}
break;
case "String":
if (GetPropertyValue(Parem, ParentId) != "")
{
SetPropertyValue<string>(Parem, ParentId, "");
}
break;
default:
break;
}
return base.OnParametersSetAsync();
}
protected void SpanToggle(Tvalue item)
{
var _clckdItemid = Convert.ToInt32(GetPropertyValue(item, Id));
_caretcss[_clckdItemid] = _caretDown[_clckdItemid] ? "caret caret-down" : "caret";
_nestedcss[_clckdItemid] = _caretDown[_clckdItemid] ? "active" : "nested";
_caretDown[_clckdItemid] = !_caretDown[_clckdItemid];
}
#region reflection methodes to get your property type, propert value and also set property value
protected string GetPropertyValue(Tvalue item, string Property)
{
if (item != null)
{
return item.GetType().GetProperty(Property).GetValue(item, null).ToString();
}
return "";
}
protected void SetPropertyValue<T>(Tvalue item, string Property, T value)
{
if (item != null)
{
item.GetType().GetProperty(Property).SetValue(item, value);
}
}
protected string GetPropertyType(Tvalue item, string Property)
{
if (item != null)
{
return item.GetType().GetProperty(Property).PropertyType.Name;
}
return null;
}
#endregion
}
Index.razor
<Treeview Tvalue="MailItem" DataSource="#MyFolder" Id="Id" Text="FolderName" ParentId="ParentId"
Expanded="Expanded" HasChildren="HasSubFolder"></Treeview>
#code{
protected class MailItem
{
public int Id { get; set; }
public string ParentId { get; set; }
public bool HasSubFolder { get; set; }
public string FolderName { get; set; }
public bool Expanded { get; set; }
}
List<MailItem> MyFolder = new List<MailItem>();
protected override Task OnInitializedAsync()
{
MyFolder.Add(new MailItem { Id = 1, FolderName = "Inbox", HasSubFolder = true, Expanded = true, ParentId = "" });
MyFolder.Add(new MailItem { Id = 2, FolderName = "Category", ParentId = "1", HasSubFolder = true, Expanded = true });
MyFolder.Add(new MailItem { Id = 3, FolderName = "Primary", ParentId = "2", HasSubFolder = false, Expanded = true });
MyFolder.Add(new MailItem { Id = 4, FolderName = "Social", ParentId = "6", HasSubFolder = false, Expanded = true });
MyFolder.Add(new MailItem { Id = 5, FolderName = "Promotion", ParentId = "6", HasSubFolder = false, Expanded = true });
MyFolder.Add(new MailItem { Id = 6, FolderName = "Demo", ParentId = "2", HasSubFolder = true, Expanded = true });
return base.OnInitializedAsync();
}
}
This is the logics behind a generic Treeview Component in Blazor.
Kindly click on this image to view the working example:

change child value from parent on timer elapse

I have a simple parent-child component :
parent :
<div class="container-fluid">
<div>
<ChildComponent RandomNumber="#RandomNumberX" ></ChildComponent>
</div>
</div>
#code {
public double RandomNumberX = 0.1;
private Timer timer = new Timer();
protected override void OnInitialized()
{
timer.Interval = 3000;
timer.Elapsed -= Set;
timer.Elapsed += Set;
timer.Start();
}
private void Set(object sender, ElapsedEventArgs e)
{
var rng = new Random();
var x = rng.Next(500, 600);
RandomNumberX = x;
}}
Child:
<svg height="100" width="100" style="position:absolute; border-color: black; ">
<g>
<circle cx="50%" cy="25" r="5" stroke="red" stroke-width="2" fill="red" />
<text x="50%" y="50" text-anchor="middle">#RandomNumber</text>
</g>
</svg>
#code {
[Parameter]
public double RandomNumber { get; set; }
protected override void OnParametersSet()
{
}
}
when running app "RandomNumber" show 0.1 and does not change while in parent component it changes on timer elapsed.
I animated a SVG clock a little while back that does this:
Note: The use of dispose and how I reduce the calculations to only when needed by checking for a second change.
Clock.razor
<div class="d-flex flex-column align-items-center">
<div class="time-sm">#timeZone.Id</div>
<svg width="#Size" height="#Size" viewBox="0 0 1000 1000">
<circle cx="#center" cy="#center" r="#radius" fill="#FaceColor" />
<ClockHand Angle="hourAngle" Color="#HourColor" Width="50" Length="0.9" />
<ClockHand Angle="minuteAngle" Color="#MinuteColor" Width="30" Length="0.95" />
<ClockHand Angle="secondAngle" Color="#SecondColor" Width="20" Length="1" />
</svg>
<div class="time-sm">#currentSecond.DateTime.ToShortTimeString()</div>
<div class="time-sm">#currentSecond.DateTime.ToString("dddd")</div>
</div>
Clock.razor.cs
public partial class Clock : ComponentBase, IDisposable
{
internal const int radius = 500;
internal const int size = 1000;
internal const int center = size / 2;
private double secondAngle = 0;
private double minuteAngle = 0;
private double hourAngle = 0;
private TimeZoneInfo timeZone;
private DateTimeOffset currentSecond;
private readonly System.Timers.Timer timer = new();
[Parameter]
public int Size { get; set; } = 50;
[Parameter]
public string FaceColor { get; set; } = "#f5f5f5";
[Parameter]
public string HourColor { get; set; } = "blue";
[Parameter]
public string MinuteColor { get; set; } = "green";
[Parameter]
public string SecondColor { get; set; } = "red";
[Parameter]
public string TimeZone { get; set; }
protected override void OnInitialized()
{
if (string.IsNullOrWhiteSpace(TimeZone) is true)
{
timeZone = TimeZoneInfo.Local;
}
else
{
timeZone = TimeZoneInfo.FindSystemTimeZoneById(TimeZone);
}
timer.Interval = 100;
timer.Elapsed += Timer_Elapsed;
UpdateClock();
timer.Start();
}
public static DateTime GmtToPacific(DateTime dateTime)
{
return TimeZoneInfo.ConvertTimeFromUtc(dateTime,
TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"));
}
private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
UpdateClock();
InvokeAsync(StateHasChanged);
}
private void UpdateClock()
{
const double radiansPer60 = 360 / 60 * Math.PI / 180;
const double radiansPer12 = 360 / 12 * Math.PI / 180;
var currentTime = TimeZoneInfo.ConvertTime(DateTimeOffset.Now, timeZone);
var roundedSencond = new DateTimeOffset(currentTime.Year, currentTime.Month, currentTime.Day, currentTime.Hour, currentTime.Minute, currentTime.Second, default);
if (roundedSencond != currentSecond)
{
currentSecond = roundedSencond;
var seconds = currentTime.Second;
var minutes = currentTime.Minute;
var hours = currentTime.Hour % 12;
secondAngle = seconds * radiansPer60;
minuteAngle = minutes * radiansPer60 + secondAngle / 60;
hourAngle = hours * radiansPer12 + minuteAngle / 12;
}
}
public void Dispose()
{
if (timer is not null)
{
timer.Dispose();
}
}
}
ClockHand.razor
<line x1="500" y1="500" x2="#X" y2="#Y" style="stroke:#Color;stroke-width:#Width" />
ClockHand.razor.cs
public partial class ClockHand : ComponentBase
{
[Parameter]
public double Angle { get; set; }
[Parameter]
public double Length { get; set; } = 1;
[Parameter]
public string Color { get; set; } = "black";
[Parameter]
public int Width { get; set; }
double X => Math.Sin(Angle) * Clock.radius * Length + Clock.center;
double Y => Math.Cos(Angle) * -Clock.radius * Length + Clock.center;
}
Useage
<Clock TimeZone="Australia/Sydney" />
or
<Clock />
TimerElapsed is not a 'normal' Blazor lifecycle event. So it won't automatically trigger a re-render. (A ButtonClick event would, for example).
So you need to call StatehasChanged, and to cater for the possibility it's on another Thread you need to InvokeAsync that.
private async void Set(object sender, ElapsedEventArgs e)
{
var rng = new Random();
var x = rng.Next(500, 600);
RandomNumberX = x;
await InvokeAsync(StatehasChnaged);
}
You should normally avoid async void but this is exactly the suituation it was created for.
Side note: timer.Elapsed -= Set; during Initialization is only half a solution.
Use #implements IDisposable on the top of your page and add
public void Dispose()
{
timer.Elapsed -= Set;
}

how can i solve 'Object reference not set to an instance of an object.' in blazor?

I giv up to solve this. i dont't know what is wrong with my code, if the problem is instance of object, i tried to give my pagging class an instance but still no clue.
This is my index class;
<DataGridComponent TItem="Employee"
DataItems="listEmployee"
Columns="columnDefinitions"
Paging="#(new PagingConfig {
Enabled = true,
CustomPager = true,
PageSize = 3
})">
<CustomPager>
<button class="btn btn-primary" #onclick="PrevPage"> Prev </button>
<span> Page
<input type="number" min="1"#bind-value="#DataGrid.currentPageNumber"/>
of #DataGrid.MaxPageNumber </span>
<button class="btn btn-primary" #onclick="NextPage"> Next </button>
</CustomPager>
</DataGridComponent>
#code{
private DataGridComponent<Employee> DataGrid;
private List<Employee> listEmployee;
private List<ColumnDefinition> columnDefinitions;
protected override void OnInitialized()
{
base.OnInitialized();
Initialize();
}
private void PrevPage()
{
DataGrid.GoToPrevPage();
}
private void NextPage()
{
DataGrid.GoToNextPage();
}
this is my DataGrid class
<div class="level">
<div class="level-left"></div>
<div class="level-right">
<div class="level-item">
#if (Paging != null && Paging.Enabled)
{
#if (Paging.CustomPager)
{
#CustomPager
}
else
{
<span #onclick="GoToPrevPage"><b><</b>Prev</span>
<span> #currentPageNumber of #Paging.MaxPageNumber(DataItems.Count)
</span>
<span #onclick="GoToNextPage"><b>Next></b></span>
}
}
</div>
</div>
</div>
#code {
[Parameter]
public int currentPageNumber { get; set; } = 1;
[Parameter]
public List<TItem> DataItems { get; set; }
[Parameter]
public List<ColumnDefinition> Columns { get; set; }
[Parameter]
public PagingConfig Paging { get; set; } = new PagingConfig();
[Parameter]
public RenderFragment CustomPager { get; set; }
public void GoToPrevPage()
{
currentPageNumber = Paging.PrevPageNumber(currentPageNumber);
}
public void GoToNextPage()
{
currentPageNumber = Paging.NextPageNumber(currentPageNumber, DataItems.Count);
}
public int MaxPageNumber { get => Paging.MaxPageNumber(DataItems.Count); }
}
and this is my my pagingconfig
public class PagingConfig
{
public bool Enabled { get; set; }
public int PageSize { get; set; }
public bool CustomPager { get; set; }
public int NumOfItemsToSkip(int pageNumber)
{
if (Enabled)
{
return (pageNumber - 1) * PageSize;
}
else
return 0;
}
public int NumOfItemsToTake(int totalItemCount)
{
if (Enabled)
{
return PageSize;
}
return totalItemCount;
}
public int PrevPageNumber(int currentPageNumber)
{
if (currentPageNumber > 1)
return currentPageNumber - 1;
else
return 1;
}
public int NextPageNumber(int currentPageNumber, int totalItemsCount)
{
if (currentPageNumber < MaxPageNumber(totalItemsCount))
{
return currentPageNumber + 1;
}
else
{
return currentPageNumber;
}
}
public int MaxPageNumber(int totalItemcount)
{
int maxPageNumber;
double numberOfPage = (double)totalItemcount / (double)PageSize;
if (numberOfPage == Math.Floor(numberOfPage))
{
maxPageNumber = (int)numberOfPage;
}
else
{
maxPageNumber = (int)numberOfPage + 1;
}
return maxPageNumber;
}
}
}
the problem is, when i tried to set enabled into true, the pagging config should get the value of true from index. But it's said not set the instance object yet. i tried to put in, the new Pagging instance into grid component but still no clue :(
DataGrid needs to be assigned. I think you want this:
<DataGridComponent #ref="DataGrid" TItem="Employee" ...>
...
</DataGridComponent>
the reference is to the variable in this line:
private DataGridComponent<Employee> DataGrid;
It's just the #ref="DataGrid" missing in the markup of your DataGridComponent. Therefore, your private DataGrid variable is null.
In Index.razor you set up a DataGridComponent instance in Razor and then declare another one in the code section private DataGridComponent<Employee> DataGrid. These are two unlinked instances of DataGridComponent. To reference DataGrid to your razor declared version you need to use #ref as below.
<DataGridComponent TItem="Employee"
DataItems="listEmployee"
Columns="columnDefinitions"
#ref = "this.DataGrid"
Paging="#(new PagingConfig {
Enabled = true,
CustomPager = true,
PageSize = 3
})">

Blazor get div position / coordinates

I'm creating a popup component and I want this to be movable. I can move it using the top / left style, but for now they are init to top:0;left:0; and so the popup appear on the top left corner of the page. I'm looking to make it appear on the center of the page and then get the Top Left coordinates of my div in ordor to manage properly my calcul after.
here is what I have now:
<div class="child-window" draggable="true" style="position:absolute; top: #(offsetY)px; left: #(offsetX)px; border-color: black;" #ondragend="OnDragEnd" #ondragstart="OnDragStart">
<div class="cw-content">
#Content
</div>
</div>
#code {
private double startX, startY, offsetX, offsetY;
protected override void OnInitialized() {
base.OnInitialized();
ResetStartPosition();
}
private void ResetStartPosition() {
//Set offsetX & offsetY to the top left div position
}
private void OnDragStart(DragEventArgs args) {
startX = args.ClientX;
startY = args.ClientY;
}
private void OnDragEnd(DragEventArgs args) {
offsetX += args.ClientX - startX;
offsetY += args.ClientY - startY;
}
}
At the moment it is possible with JS only
public class BoundingClientRect
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public double Top { get; set; }
public double Right { get; set; }
public double Bottom { get; set; }
public double Left { get; set; }
}
private async Task OnElementClick(MouseEventArgs e)
{
var result = await JSRuntime.InvokeAsync<BoundingClientRect>("MyDOMGetBoundingClientRect", MyElementReference);
var x = (int) (e.ClientX - result.Left);
var y = (int) (e.ClientY - result.Top);
// now you can work with the position relative to the element.
}
and
<script> MyDOMGetBoundingClientRect = (element, parm) => { return element.getBoundingClientRect(); }; </script>

How can I modify this to have a Google-like paging?

My controller class contains
var Paged = new PaginatedList<Products>(SideBar, page ?? 0, pageSize);
if (Request.IsAjaxRequest())
{
return PartialView("~/Views/Shared/_Grid.cshtml", Paged);
}
return View(Paged);
the PaginatedList is
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize)
{
PageIndex = pageIndex;
PageSize = pageSize;
TotalCount = source.Count();
TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
}
public bool HasPreviousPage
{
get
{
return (PageIndex > 0);
}
}
public bool HasNextPage
{
get
{
return (PageIndex + 1 < TotalPages);
}
}
}
And my view is
<div class="pagination-container">
<nav class="pagination">
<ul>
#for (int i = 0; i < Model.TotalPages; i++)
{
<li><a href="#Url.Action("Index", "Home", new { page = i})"
class="#(i == Model.PageIndex ? "current-page" : "")">#(i + 1)</a></li>
}
</ul>
</nav>
<nav class="pagination-next-prev">
<ul>
#if (Model.HasPreviousPage) {
<li></li>
}
#if (Model.HasNextPage) {
<li></li>
}
</ul>
</nav>
<div>
Page #(Model.PageIndex + 1) of #Model.TotalPages
</div>
</div>
One problem with the view above, is that it creates numeric pages equal to the page sizes within model. If the model has 6 pages the result is
What will happen if i have 100 Model.Pages ?
You need to make sure that you separate you're not returning all the data in the model - you return only return the page size, at most, and have a separate property to store the total count.
For instance:
public class PageResult<T> where T : class
{
public PageResult(IEnumerable<T> items, int page = 1, int pageSize = 10)
{
this.Items = items;
this.PageSize = pageSize;
this.PageIndex = page;
this.PaginateData(page);
}
public IEnumerable<T> Items { get; private set; }
public int PageSize { get; private set; }
public int PageIndex { get; private set; }
public int Total { get; private set; }
public int TotalPages
{
get
{
return Math.Max((int)Math.Ceiling((Total / (double)PageSize)), 1);
}
}
private int _MaxPagesToDisplay = 10;
public int MaxPagesToDisplay
{
get { return _MaxPagesToDisplay; }
set { _MaxPagesToDisplay = value; }
}
public int PagesToDisplay
{
get
{
return Math.Min(this.MaxPagesToDisplay, TotalPages);
}
}
public void PaginateData(int page)
{
if(this.Items == null) return;
this.Total = this.Items.Count();
this.Items = this.Items.Skip(this.PageSize * this.PageIndex - 1).Take(this.PageSize);
}
}
This will mean you can return for example 1 million results and set the Total to 1 million, but only insert 10 into the Items collection. You can use Linq to paginate the data into this collection. I added the method PaginateData which does it for you.
Then you can update your view:
#for (int i = 1; i <= Model.PagesToDisplay; i++)
{
<li><a href="#Url.Action("Index", "Home", new { page = i})"
class="#(i == Model.PageIndex ? "current-page" : "")">#(i)</a></li>
}
You can then use the Total field to display the total count on the page.

Categories

Resources