Reputation: 49
Beforehand i apologize for any mistakes, as English is not my first language. I'm creating a razor component library to be implemented in an existent .NET 7 Blazor Server application. Basically a simple IMAP e-mail client, I'm using MailKit library. I have used the MailKit documentation example for windows forms, and adjusted to work in razor. It is populating the folders as expected, but when I try to update an MessageInfo item, as an example, changing to flagged or unflagged, I'm trying to udate the UI accordingly, but the changes are only applied on page refresh.
I tried StateHasChanged, InvokeAsync(StateHasChanged), event Action, all without success. This is what i got so far, serviçe:
using BlazorEmailLib.Models;
using MailKit;
using MailKit.Net.Imap;
namespace BlazorEmailLib.Services;
public interface IMessageListService
{
event EventHandler<MessageSelectedEventArgs>? MessageSelected;
event Action? OnMessagesUpdated;
Task OpenFolderAsync(IMailFolder folder);
void AddMessageSummaries(IMailFolder folder, IEnumerable<IMessageSummary> summaries);
void SelectMessage(MessageInfo messageInfo);
List<MessageInfo> GetMessages();
Task ToggleFlagAsync(MessageInfo messageInfo, MessageFlags flags);
}
public class MessageListService : IMessageListService
{
private static readonly FetchRequest _request = new(MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope | MessageSummaryItems.Flags | MessageSummaryItems.BodyStructure);
private const int _batchSize = 512;
private readonly List<MessageInfo> _messages = new();
private IMailFolder? _folder;
public event EventHandler<MessageSelectedEventArgs>? MessageSelected;
public event Action? OnMessagesUpdated;
public async Task OpenFolderAsync(IMailFolder folder)
{
if (this._folder != null)
{
this._folder.MessageFlagsChanged -= OnMessageFlagsChanged;
this._folder.MessageExpunged -= OnMessageExpunged;
this._folder.CountChanged -= OnCountChanged;
}
folder.MessageFlagsChanged += OnMessageFlagsChanged;
folder.MessageExpunged += OnMessageExpunged;
this._folder = folder;
lock (_messages)
{
_messages.Clear();
}
try
{
if (!folder.IsOpen)
await folder.OpenAsync(FolderAccess.ReadWrite);
if (folder.Count > 0)
{
var summaries = await folder.FetchAsync(0, -1, _request);
AddMessageSummaries(folder, summaries);
}
}
catch (ImapCommandException ex) when (ex.Message.Contains("NO"))
{
Console.WriteLine("Algumas mensagens não existem mais. Atualizando a lista...");
await folder.OpenAsync(FolderAccess.ReadWrite);
}
catch (Exception ex)
{
Console.WriteLine($"Erro inesperado: {ex.Message}");
}
folder.CountChanged += OnCountChanged;
}
public void AddMessageSummaries(IMailFolder folder, IEnumerable<IMessageSummary> summaries)
{
if (folder != this._folder)
return;
foreach (var message in summaries)
{
var info = new MessageInfo(message);
_messages.Add(info);
}
if (_messages.Count < folder.Count)
FetchNewMessages(folder);
}
private void FetchNewMessages(IMailFolder folder)
{
Task.Run(async () =>
{
if (!folder.IsOpen)
await folder.OpenAsync(FolderAccess.ReadWrite);
if (folder.Count > 0)
{
int currentCount;
lock (_messages)
{
currentCount = _messages.Count;
}
var summaries = await folder.FetchAsync(currentCount, Math.Min(folder.Count - 1, currentCount + _batchSize - 1), _request);
AddMessageSummaries(folder, summaries);
}
});
}
private void OnMessageFlagsChanged(object? sender, MessageFlagsChangedEventArgs e)
{
lock (_messages)
{
if (e.Index < _messages.Count)
{
var info = _messages[e.Index];
info.Flags = e.Flags;
}
}
}
private void OnMessageExpunged(object? sender, MessageEventArgs e)
{
lock (_messages)
{
if (e.Index < _messages.Count)
{
_messages.RemoveAt(e.Index);
}
}
}
private void OnCountChanged(object? sender, EventArgs e)
{
var folder = (IMailFolder)sender!;
FetchNewMessages(folder);
}
public void SelectMessage(MessageInfo messageInfo)
{
if (_folder != null)
{
MessageSelected?.Invoke(this, new MessageSelectedEventArgs(_folder, messageInfo.Summary.UniqueId, messageInfo.Summary.Body));
}
}
public List<MessageInfo> GetMessages()
{
lock (_messages)
{
return new List<MessageInfo>(_messages);
}
}
public async Task ToggleFlagAsync(MessageInfo messageInfo, MessageFlags flags)
{
if (_folder != null)
{
if (messageInfo.Flags.HasFlag(flags))
{
await _folder.RemoveFlagsAsync(messageInfo.Summary.UniqueId, flags, true);
}
else
{
await _folder.AddFlagsAsync(messageInfo.Summary.UniqueId, flags, true);
}
// Notifica que as mensagens mudaram
OnMessagesUpdated?.Invoke();
}
}
}
public class MessageSelectedEventArgs : EventArgs
{
public IMailFolder Folder { get; }
public UniqueId UniqueId { get; }
public BodyPart Body { get; }
public MessageSelectedEventArgs(IMailFolder folder, UniqueId uniqueId, BodyPart body)
{
Folder = folder;
UniqueId = uniqueId;
Body = body;
}
}
And the razor component:
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.Extensions.Logging
@using BlazorEmailLib.Services
@using BlazorEmailLib.Helpers
@using BlazorEmailLib.Models
@using MailKit
@using MailKit.Net.Imap;
@using MimeKit
@inject IImapClientService ImapClientService
@inject IMessageListService MessageListService
@inject ILogger<MessageListView> Logger
@namespace BlazorEmailLib.Components
<div>
@if (IsLoading)
{
foreach (var i in Enumerable.Range(0, 10))
{
<div class="email-item">
<input type="checkbox" class="skeleton" />
<div class="email-item-info">
<div class="email-item-info__header skeleton" style="height: 20px; margin-bottom: 0.25rem;">
</div>
<div class="email-item-subject skeleton" style="height: 20px;">
</div>
<div class="email-item-details skeleton" style="height: 40px;">
</div>
</div>
</div>
}
}
else if (_messages != null && _messages.Any())
{
@foreach (var message in _messages)
{
<div @key="@message.Summary.UniqueId"
class="email-item @(message.Flags.HasFlag(MessageFlags.Seen) ? "seen" : "") @(IsCurrent ? "email-item__selected":"")">
<input type="checkbox" name="@message.Summary.UniqueId"
id="@message.Summary.UniqueId" value="@message.Summary.UniqueId"
checked="@IsSelected"
@onchange="async (e) => await IsSelectedChanged.InvokeAsync(e.Value != null && (bool)e.Value)" />
<div class="email-item-info" @onclick=HandleClick>
<div class="email-item-info__header">
<span class="email-item-info__header-from">@message.Summary.Envelope.From.Mailboxes.FirstOrDefault()!.Address</span>
<span class="email-item-info__header-datetime">
@if (message.Summary.Envelope.Date?.Date == DateTimeOffset.Now.Date)
{
@message.Summary.Envelope.Date?.DateTime.ToString("HH:mm")
}
else
{
@message.Summary.Envelope.Date?.DateTime.ToString("dd/MM/yyyy")
}
</span>
</div>
<div class="email-item-subject">
@CommonHelpers.TruncateText(message.Summary.NormalizedSubject)
</div>
<div class="email-item-details">
<span>@CommonHelpers.ConvertMessageSize(message.Summary.Size.GetValueOrDefault())</span>
<span>
@if (message.Summary.Attachments.Any(a => a.IsAttachment))
{
<i class="fa-solid fa-paperclip"></i>
}
@if (message.Summary.Flags != null)
{
@if (message.Summary.Flags.Value.HasFlag(MessageFlags.Answered))
{
<i class="fa fa-check"></i>
}
<span @onclick:stopPropagation
@onclick="@(async () => await HandleFlagClick(message, MessageFlags.Flagged))">
<i class="fa fa-flag icon-button @(message.Flags.HasFlag(MessageFlags.Flagged) ? "text-danger" : "")"></i>
</span>
}
</span>
</div>
</div>
</div>
}
}
else
{
<p>No messages found.</p>
}
</div>
@code {
[Parameter] public IMailFolder? Folder { get; set; }
private List<MessageInfo>? _messages;
private bool IsLoading { get; set; } = true;
private bool IsCurrent { get; set; }
public bool IsSelected { get; set; }
public EventCallback<bool> IsSelectedChanged { get; set; }
protected override async Task OnParametersSetAsync()
{
if (Folder != null)
{
IsLoading = true;
await MessageListService.OpenFolderAsync(Folder);
_messages = new List<MessageInfo>(MessageListService.GetMessages());
IsLoading = false;
}
}
protected override void OnInitialized()
{
MessageListService.OnMessagesUpdated += async () => await InvokeAsync(StateHasChanged);
}
private async void HandleMessagesUpdated()
{
await InvokeAsync(StateHasChanged);
}
private void SelectMessage(MessageInfo messageInfo)
{
MessageListService.SelectMessage(messageInfo);
}
private async Task HandleClick(MouseEventArgs e)
{
}
private async Task HandleFlagClick(MessageInfo message, MessageFlags flags)
{
await MessageListService.ToggleFlagAsync(message, flags);
// Atualiza o estado do objeto (Blazor só deteta mudanças em propriedades e não referências)
message.Flags = message.Flags.HasFlag(flags)
? message.Flags & ~flags // Remove flag
: message.Flags | flags; // Adiciona flag
// Força o Blazor a redesenhar o componente
await InvokeAsync(StateHasChanged);
}
public void Dispose()
{
MessageListService.OnMessagesUpdated -= HandleMessagesUpdated;
}
}
I appreciate any help, thank you in advance.
Upvotes: 0
Views: 70
Reputation: 49
The problem was that when using fa icons the fill/color is set when page renders, the icon is switched for an SVG, so this:
<div @onclick:stopPropagation
class="@(Message.Flags.HasFlag(MessageFlags.Flagged) ? "flag-red" : "")"
@onclick="@(async () => await HandleFlagClick(Message, MessageFlags.Flagged))" style="cursor: pointer;">
<i class="fa fa-flag" ></i>
</div>
becomes:
<div class="" style="cursor: pointer;"><!--!--><svg class="svg-inline--fa fa-flag" aria-hidden="true" f..."></path></svg><!-- <i class="fa fa-flag"></i> Font Awesome fontawesome.com --></div>
I changed my CSS to target the fill
.flag-red *{
fill: var(--error);
}
Now it works as expected.
Thank you all to point me to the right direction.
Upvotes: 0
Reputation: 30310
It's hard to piece together your code because I don't have much context, or an MRE. However, any code peppered with calls to StateHasChanged
has some fundimental logic issues.
In your case, it looks like your basic problem is switching between async and sync method patterns.
You should be Async all the Way.
This method kicks off some async work, but returns a null. There's no awaiting, so all the code that calls this can complete before the async stuff you're doing inside it completes.
private void FetchNewMessages(IMailFolder folder)
{
//Do async stuff
}
Turn this into an async method, and await it. If you name all your async methods as I've done below, you'll know when you need to await a method.
Shound be:
private async Task FetchNewMessagesAsync(IMailFolder folder)
{
//Do async stuff
// You no longer need Task.Run
}
Once you have the async correctly awaited, all the calls to StateHasChanged
will be unnecessary, except those in event handlers.
Trying to solving this problem with debugging doesn't work. You need to add a lot of Console
or Debug
writes into your code to see the executing sequence in real time. You can add something like this into the page/component markup to see when the page actually renders:
@{
Console.WriteLine("Rendering Page");
}
On your comment that the styling is not applied, here's my minimal reproduction of your code:
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<div class="@(_inDanger ? "text-danger" : string.Empty)" @onclick:stopPropagation @onclick="@(async () => await HandleFlagClick())">
Click Me
</div>
<div class="@(_inDanger ? "text-danger" : string.Empty)" @onclick:stopPropagation @onclick="HandleFlagClick">
Click Me
</div>
@code {
private bool _inDanger;
private async Task HandleFlagClick()
{
// Do so async work
await Task.Delay(100);
_inDanger = !_inDanger;
}
}
Copy it into a page. It works.
Upvotes: 1