英文:
Blazor (Mudblazor) Table Countdown Property
问题
我有一个在MudBlazor应用程序中的表格,其中包含一个“停止时间”的值。我正在在表格中显示的一列是“剩余时间”,它是由一个函数填充的,该函数根据当前日期和“停止时间”值来计算剩余时间。我想知道是否有一种简单的方式来使这个值动态更新,比如每隔一分钟更新一次。目前的设置方式是您可以点击一个刷新按钮,但这样做会相当耗时,我不太想设置每分钟完全刷新的方式。
<MudTable Items="ActiveForwards" Hover="true" SortLabel="Sort By" Elevation="0" Filter="new Func<UniversalObject,bool>(FilterResults)">
<ToolBarContent>
<MudTextField @bind-Value="searchString1" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</ToolBarContent>
<HeaderContent>
<MudTh><MudTableSortLabel InitialDirection="SortDirection.Ascending" SortBy="new Func<UniversalObject, object>(x=>x.DisplayName)">Mailbox</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<UniversalObject, object>(x=>x.EmailAddress)">Email Address</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<UniversalObject, object>(x=>x.User.ForwardToDisplay)">Forward Mailbox</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<UniversalObject, object>(x=>x.User.StopTime.ToString())">Stop Time</MudTableSortLabel></MudTh>
<MudTh>Time Left</MudTh>
<MudTh></MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Display Name">@context.DisplayName</MudTd>
<MudTd DataLabel="Email Address">@context.EmailAddress</MudTd>
<MudTd DataLabel="Forward To Display Name">@context.User.ForwardToDisplay</MudTd>
<MudTd DataLabel="Stop Time">@context.User.StopTime.ToString()</MudTd>
<MudTd DataLabel="Time Left"><h3>@CountdownTimer(context)</h3></MudTd>
<MudTd><MudIcon Icon="@Icons.Material.Filled.Refresh" Variant="Variant.Filled" Color="@(context.User.DeliverToMailboxAndForward ? Color.Primary : Color.Default)" Size="Size.Medium" /></MudTd>
<MudTd DataLabel="Actions"><MudIconButton Icon="@Icons.Material.Filled.Delete" @onclick="() => HandleRemoveAction(context)"></MudIconButton></MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[]{10, 50, 100}" />
</PagerContent>
</MudTable>
private string CountdownTimer(UniversalObject forward)
{
if (forward.User.StopTime != null)
{
var timeLeft = forward.User.StopTime - DateTime.Now;
if (timeLeft.Value.Days > 0)
{
return $"{timeLeft.Value.Days}D {timeLeft.Value.Hours}H {timeLeft.Value.Minutes}M";
}
if (timeLeft.Value.Hours > 0)
{
return $"{timeLeft.Value.Hours}H {timeLeft.Value.Minutes}M";
}
return $"{timeLeft.Value.Minutes}M";
}
return "∞";
}
英文:
I have a table in a MudBlazor application that contains a value for Stop Time. One of the columns I am displaying on the table is Time Left which is populated by a function that calculates the time left based on the current date and the Stop Time value. I am wondering if there is an easy way to make this value update dynamically, say once every minute. The way I have it setup now you can hit a refresh button, but it is quite taxing and I don't really want to setup a full refresh every minute.
<MudTable Items="ActiveForwards" Hover="true" SortLabel="Sort By" Elevation="0" Filter="new Func<UniversalObject,bool>(FilterResults)">
<ToolBarContent>
<MudTextField @bind-Value="searchString1" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</ToolBarContent>
<HeaderContent>
<MudTh><MudTableSortLabel InitialDirection="SortDirection.Ascending" SortBy="new Func<UniversalObject, object>(x=>x.DisplayName)">Mailbox</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<UniversalObject, object>(x=>x.EmailAddress)">Email Address</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<UniversalObject, object>(x=>x.User.ForwardToDisplay)">Forward Mailbox</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<UniversalObject, object>(x=>x.User.StopTime.ToString())">Stop Time</MudTableSortLabel></MudTh>
<MudTh>Time Left</MudTh>
<MudTh></MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Display Name">@context.DisplayName</MudTd>
<MudTd DataLabel="Email Address">@context.EmailAddress</MudTd>
<MudTd DataLabel="Forward To Display Name">@context.User.ForwardToDisplay</MudTd>
<MudTd DataLabel="Stop Time">@context.User.StopTime.ToString()</MudTd>
<MudTd DataLabel="Time Left"><h3>@CountdownTimer(context)</h3></MudTd>
<MudTd><MudIcon Icon="@Icons.Material.Filled.Refresh" Variant="Variant.Filled" Color="@(context.User.DeliverToMailboxAndForward ? Color.Primary : Color.Default)" Size="Size.Medium" /></MudTd>
<MudTd DataLabel="Actions"><MudIconButton Icon="@Icons.Material.Filled.Delete" @onclick="() => HandleRemoveAction(context)"></MudIconButton></MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[]{10, 50, 100}" />
</PagerContent>
</MudTable>
}
private string CountdownTimer(UniversalObject forward)
{
if (forward.User.StopTime != null)
{
var timeLeft = forward.User.StopTime - DateTime.Now;
if (timeLeft.Value.Days > 0)
{
return $"{timeLeft.Value.Days}D {timeLeft.Value.Hours}H {timeLeft.Value.Minutes}M";
}
if (timeLeft.Value.Hours > 0)
{
return $"{timeLeft.Value.Hours}H {timeLeft.Value.Minutes}M";
}
return $"{timeLeft.Value.Minutes}M";
}
return "\u221e";
}
答案1
得分: 0
以下是您要翻译的内容:
"Firstly,
don't run Task.Run
unless you have a very specific reason to do so (and understand why). You can quickly get into trouble. Code that interacts with the UI has to run within the UI context thread. Take this case: the application has switched the timer task to a separate threadpool thread. Try calling StateHasChanged
directly in the handler.
private async void OnTimerElapsed(object? sender, ElapsedEventArgs e)
{
await this.RefreshData();
}
private Task RefreshData()
{
this.Message = DateTime.Now.ToLongTimeString();
this.StateHasChanged();
return Task.CompletedTask;
}
Run the application without debug and it crashes. Run in debug mode and you get:
The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state."
Update `RefreshData`:
```csharp
private async Task RefreshData()
{
this.Message = DateTime.Now.ToLongTimeString();
await this.InvokeAsync(StateHasChanged);
}
``` and `await this.InvokeAsync(StateHasChanged)` ensures that `StateHasChanged` is run on the `Dispatcher` thread.
Secondly,
`_timer.Elapsed` is an event. It's fire and forget. The return type for any event handler is `void`. If you need to run any async code, you declare it as `async`. It becomes a `Task` without an owner, so its ownership defaults to the application. Any exceptions are bubbled up to the application and crash it. You've seen exactly that where `StateHasChanged` was run on the wrong thread and crashed the application.
Go back to the original `OnTimerElapsed` with the direct call to `StateHasChanged`:
```csharp
private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
=> _refreshTask = this.RefreshData();
private Task RefreshData()
{
this.Message = DateTime.Now.ToLongTimeString();
this.StateHasChanged();
return Task.CompletedTask;
}
Run the application without debug, and while the counter doesn't update, it doesn't crash: you can navigate to other pages,... Run in debug mode, and you'll get the exception (because the debugger traps exceptions).
A modification to the original code
Whilst writing this explanation, I realized you use this pattern for all event handlers in the UI to ensure the handler gets run on the UI/Dispatcher thread.
private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
=> _refreshTask = InvokeAsync(this.RefreshData);
So the new version looks like this:
private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
=> _refreshTask = InvokeAsync(this.RefreshData);
private Task RefreshData()
{
this.Message = DateTime.Now.ToLongTimeString();
this.StateHasChanged();
return Task.CompletedTask;
}
```"
<details>
<summary>英文:</summary>
Here's a simple demo page that shows how to run a timer Task in a page.
```csharp
@page "/"
@using System.Timers
@implements IDisposable
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<div class="alert alert-info">
@this.Message
</div>
@code {
private System.Timers.Timer _timer = new System.Timers.Timer(2000);
private Task _refreshTask = Task.CompletedTask;
private string? Message;
protected override void OnInitialized()
{
_timer.AutoReset = true;
_timer.Elapsed += this.OnTimerElapsed;
_timer.Enabled = true;
_refreshTask = this.RefreshData();
}
private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
=> _refreshTask = InvokeAsync(this.RefreshData);
private Task RefreshData()
{
this.Message = DateTime.Now.ToLongTimeString();
this.StateHasChanged();
return Task.CompletedTask;
}
public void Dispose()
=> _timer.Elapsed -= this.OnTimerElapsed;
}
You've questioned the reason for _refreshTask = this.RefreshData();
and replaced it with:
Task.Run(RefreshData);
Firstly,
don't run Task.Run
unless you have a very specific reason to do so (and understand why). You can quickly get into trouble. Code that interacts with the UI has to run within the UI context thread. Take this case: the application has switched the timer task to a separate threadpool thread. Try calling StateHasChanged
directly in the handler.
private async void OnTimerElapsed(object? sender, ElapsedEventArgs e)
{
await this.RefreshData();
}
private Task RefreshData()
{
this.Message = DateTime.Now.ToLongTimeString();
this.StateHasChanged();
return Task.CompletedTask;
}
Run the application without debug and it crashes. Run in debug mode and you get:
The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.'
Update RefreshData
:
private async Task RefreshData()
{
this.Message = DateTime.Now.ToLongTimeString();
await this.InvokeAsync(StateHasChanged);
}
and await this.InvokeAsync(StateHasChanged)
ensures that StateHasChanged
is run on the Dispatcher
thread.
Secondly,
_timer.Elapsed
is a event. It's fire and forget. The return type for any event hanlder is void
. If you need to run any async code you declare it as async
. It becomes a Task
without an owner, so it's ownership defaults to the application. Any exceptions are bubbled up to the application and crash it. You've seen exacly that where StateHasChanged
was run on the wrong thread and crashed the application.
Go back to the original OnTimerElapsed
with the direct call to StateHasChanged
:
private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
=> _refreshTask = this.RefreshData();
private Task RefreshData()
{
this.Message = DateTime.Now.ToLongTimeString();
this.StateHasChanged();
return Task.CompletedTask;
}
Run the application without debug and while the counter doesn't update, it doesn't crash: you can navigate to other pages,... Run in debug mode and you'll get the exception (because the debugger traps exceptions).
A modification to the original code
Whilst writing this explanation I realised you use this pattern for all event handlers in the UI to ensure the handler gets run on UI/Dispatcher thread.
private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
=> _refreshTask = InvokeAsync(this.RefreshData);
So the new version looks like this:
private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
=> _refreshTask = InvokeAsync(this.RefreshData);
private Task RefreshData()
{
this.Message = DateTime.Now.ToLongTimeString();
this.StateHasChanged();
return Task.CompletedTask;
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论