locked
server blazor | Handle event when a user left the page RRS feed

  • Question

  • User2110873642 posted

    how can i detect when a user left the page? (moved to another page)

    when the user left, i want to signal the server, so that it can show other users, that someone left. i tried a heartbeat in a infinite loop, but it keeps running forever after the user left.

    Thursday, July 23, 2020 5:59 PM

Answers

  • User475983607 posted

    Let Core's DI framework handle the singleton's life time.

    namespace BlazorServerDemo.Services
    {
        public interface IUserTracker
        {
            void Add(string id, string info);
            void Remove(string id);
            List<string> Get();
        }
    
        public class UserTracker : IUserTracker
        {
            private readonly ILogger<UserTracker> _logger;
    
            private Dictionary<string, string> UserDictionary;
            public UserTracker(ILogger<UserTracker> logger)
            {
                _logger = logger;
                UserDictionary = new Dictionary<string, string>();
            }
    
            public void Add(string id, string info)
            {
                if (!UserDictionary.Any(m => m.Key == id))
                {
                    _logger.LogInformation($"Adding new user {id} : {info}");
                    UserDictionary.Add(id, info);
                }
                else
                {
                    _logger.LogInformation($"Add failed.  The user already exists {id} : {info}");
                }
                
            }
    
            public void Remove(string id)
            {
                if (UserDictionary.Any(m => m.Key == id))
                {
                    UserDictionary.Remove(id);
                    _logger.LogInformation($"Deleted new user {id}");
                }
                else
                {
                    _logger.LogInformation($"Delete failed.  The user does not exist {id}");
                }         
            }
    
            public List<string> Get()
            {
                return UserDictionary.Select(m => m.Key).ToList();
            }
    
        }
    }

    ConfigureServices

    services.AddSingleton<IUserTracker, UserTracker>();
    @page "/usertracker"
    @using BlazorServerDemo.Services
    @implements IDisposable
    @inject IUserTracker uTracker
    
    <h3>User tracker Demo</h3>
    
    <div>
        Connected users = @connectedUsers
    </div>
    
    @code {
    
        private string userId;
        private string userInfo;
        private int connectedUsers;
    
        private bool _disposed = false;
        private bool runTimer = true;
    
    
        public async void StartTimerAsync()
        {
            while (runTimer)
            {
                connectedUsers = uTracker.Get().Count();
                this.StateHasChanged();
                await Task.Delay(1000);
            }
        }
    
    
        protected override void OnInitialized()
        {
            if (string.IsNullOrEmpty(userId))
            {
                userId = Guid.NewGuid().ToString();
                userInfo = DateTime.Now.ToString();
                uTracker.Add(userId, userInfo);
                connectedUsers = uTracker.Get().Count();
                StartTimerAsync();
            }
        }
    
        public void Dispose() => Dispose(true);
    
        public void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }
    
            if (disposing)
            {
                uTracker.Remove(userId);
                runTimer = false;
            }
    
            _disposed = true;
        }
    }
    

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Thursday, July 23, 2020 10:49 PM

All replies

  • User475983607 posted

    how can i detect when a user left the page? (moved to another page)

    What does "left the page" mean?  The user went to another page in the same site?  If so, remember Blazor is a client application.  The screen changes but the pipeline does not.

    when the user left, i want to signal the server, so that it can show other users, that someone left. i tried a heartbeat in a infinite loop, but it keeps running forever after the user left.

    There must be something wrong with the heartbeat code.  Share code that reproduces the issue.

    Thursday, July 23, 2020 6:11 PM
  • User2110873642 posted

    What does "left the page" mean? 

    I basically mean whenever the .razer view is not seen.

    There must be something wrong with the heartbeat code.  Share code that reproduces the issue.

    This is on the .razer page, that i want to monitor if its opened at the browser.

    @code {
    
        public async void StartTimerAsync()
        {
            while (true)
            {
                SomeSingleTonClass.DoHeartBeat();
                this.StateHasChanged();
                await Task.Delay(1);
            }
        }
    
        protected override void OnInitialized() => StartTimerAsync();
    }

    but when i monitor the Console. the heartbeat is still continueing after the user stopped viewing the .razer component. Even when completely closing the browser, the heartbeat 'never' stops.

    Thursday, July 23, 2020 10:04 PM
  • User475983607 posted

    Let Core's DI framework handle the singleton's life time.

    namespace BlazorServerDemo.Services
    {
        public interface IUserTracker
        {
            void Add(string id, string info);
            void Remove(string id);
            List<string> Get();
        }
    
        public class UserTracker : IUserTracker
        {
            private readonly ILogger<UserTracker> _logger;
    
            private Dictionary<string, string> UserDictionary;
            public UserTracker(ILogger<UserTracker> logger)
            {
                _logger = logger;
                UserDictionary = new Dictionary<string, string>();
            }
    
            public void Add(string id, string info)
            {
                if (!UserDictionary.Any(m => m.Key == id))
                {
                    _logger.LogInformation($"Adding new user {id} : {info}");
                    UserDictionary.Add(id, info);
                }
                else
                {
                    _logger.LogInformation($"Add failed.  The user already exists {id} : {info}");
                }
                
            }
    
            public void Remove(string id)
            {
                if (UserDictionary.Any(m => m.Key == id))
                {
                    UserDictionary.Remove(id);
                    _logger.LogInformation($"Deleted new user {id}");
                }
                else
                {
                    _logger.LogInformation($"Delete failed.  The user does not exist {id}");
                }         
            }
    
            public List<string> Get()
            {
                return UserDictionary.Select(m => m.Key).ToList();
            }
    
        }
    }

    ConfigureServices

    services.AddSingleton<IUserTracker, UserTracker>();
    @page "/usertracker"
    @using BlazorServerDemo.Services
    @implements IDisposable
    @inject IUserTracker uTracker
    
    <h3>User tracker Demo</h3>
    
    <div>
        Connected users = @connectedUsers
    </div>
    
    @code {
    
        private string userId;
        private string userInfo;
        private int connectedUsers;
    
        private bool _disposed = false;
        private bool runTimer = true;
    
    
        public async void StartTimerAsync()
        {
            while (runTimer)
            {
                connectedUsers = uTracker.Get().Count();
                this.StateHasChanged();
                await Task.Delay(1000);
            }
        }
    
    
        protected override void OnInitialized()
        {
            if (string.IsNullOrEmpty(userId))
            {
                userId = Guid.NewGuid().ToString();
                userInfo = DateTime.Now.ToString();
                uTracker.Add(userId, userInfo);
                connectedUsers = uTracker.Get().Count();
                StartTimerAsync();
            }
        }
    
        public void Dispose() => Dispose(true);
    
        public void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }
    
            if (disposing)
            {
                uTracker.Remove(userId);
                runTimer = false;
            }
    
            _disposed = true;
        }
    }
    

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Thursday, July 23, 2020 10:49 PM
  • User2110873642 posted

    i got it to work!

    thank you so much!

    Thursday, July 23, 2020 10:58 PM
  • User2110873642 posted

    I upgraded your code to support multiple 'lobbies'. now i can track and connect users all over my application.

    using Microsoft.AspNetCore.Components;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Cryptography.X509Certificates;
    using System.Threading.Tasks;
    
    namespace MultiPlayer.Services
    {
        public interface IUserTracker
        {
            void Add(string GUID, string LobbyID, string Username);
            void Remove(string GUID);
            List<UserTracker.Lobby.Peer> GetPeers(string LobbyID);
        }
    
        public class UserTracker : IUserTracker
        {
            private readonly ILogger<UserTracker> _logger;
    
            private List<Lobby> Lobbies = new List<Lobby>();
            public class Lobby
            {
                public string LobbyID;
                public List<Peer> Peers = new List<Peer>();
                public class Peer
                {
                    public string GUID;
                    public string Username;
                }
            }
    
    
            public UserTracker(ILogger<UserTracker> logger)
            {
                _logger = logger;
            }
    
            public void Add(string GUID, string LobbyID, string Username)
            {
                if (!Lobbies.Any(c => c.LobbyID == LobbyID))
                {
                    Lobbies.Add(new Lobby() { LobbyID = LobbyID });
                }
                if (!Lobbies.Where(c => c.LobbyID == LobbyID).First().Peers.Any(c => c.GUID == GUID))
                {
                    Lobbies.Where(c => c.LobbyID == LobbyID).First().Peers.Add(new Lobby.Peer() { GUID = GUID, Username = Username });
                }
    
            }
    
            public void Remove(string GUID)
            {
                foreach (var Lobby in Lobbies)
                {
                    if (Lobby.Peers.Any(c => c.GUID == GUID))
                    {
                        Lobby.Peers.Remove(Lobby.Peers.First(c => c.GUID == GUID));
                    }
                }
            }
    
            public List<Lobby.Peer> GetPeers(string LobbyID)
            {
                return Lobbies.First(c => c.LobbyID == LobbyID).Peers.ToList();
            }
    
        }
    }
    

    Friday, July 24, 2020 12:18 AM
  • User2110873642 posted

    i have a new issue with your code. it does not remove the user when you use the browser back button to leave the page.

    this breaks my app. what do i do?

    Saturday, July 25, 2020 11:11 PM
  • User475983607 posted

    fazioliamboina

    i have a new issue with your code. it does not remove the user when you use the browser back button to leave the page.

    this breaks my app. what do i do?

    That's not true.  The original design take advantage of the standard OnInitialized and Dispose events.  Pressing the back button causes Dispose to execute on the current page and OnInitialized to execute when loading the "back" page. 

    There must be a bug(s) in your extended version of the service.  BTW, I wrote a much cleaner version of the lobby requirement and the new services handles the back button as well.  Basically, I the design still uses a Dictionary however, the "lobby" is the key and the value is an collection. 

    I recommend basic debugging; set break points, implement logging, write to the browser console, review your code, read the docs so you understand how the framework functions.

    There is a quirky or at least unexpected behavior where OnInitialized and Dispose runs twice when using ServerPrerendered (_Host.razor) mode.  Keep in mind, that only affects the code on the initial load or clicking refresh button.  OnInitialized and Dispose  do NOT run twice when clicking the back button.   Anyway, change the mode to "Server" to stop the behavior or design your code to handle the situation if the behavior is causing an issue in your code.  

    <component type="typeof(App)" render-mode="Server" />

    Sunday, July 26, 2020 12:50 PM
  • User2110873642 posted

    i figured out what happened, but i still like your advice.

    lets forget about the usertracker, because the issue isnt there.

    it has to do with querystrings

    if the user goes to :                  /page?p=lara

    and then the user goes to :     /page?p=john

    //then everything still works fine.

    but if the user uses the back button, the page does not update to Lara, but it still stays on John.

    I know how to fix it with the navigationmanager.navigateto(... , true) //force reload

    but i dont know how to do it with the back button.

    Sunday, July 26, 2020 2:24 PM
  • User475983607 posted

    fazioliamboina

    but if the user uses the back button, the page does not update to Lara, but it still stays on John.

    This is the expected browser behavior.   I wonder why is John is allowed to access Lara's profile?  Seems like a security vulnerability.  What is the use case?

    fazioliamboina

    I know how to fix it with the navigationmanager.navigateto(... , true) //force reload

    I'm not convinced navigation is the right tools for the job.  It seems the main issue you are trying to solve is identifying the user.  This programming problem has been solved long ago with authentication cookies.  Change the design so that the user logs in or create an anonymous login where the system assigns the user a unique ID.  Stored the ID in a cookie along with other meta data related to the user.   In one of your other thread I illustrated how to create a cookie and fetch the cookie.  

    Also take a look at official Blazor state management documentation.  Perhaps you can find an option that best fits your unknown use case.

    Sunday, July 26, 2020 2:54 PM
  • User2110873642 posted

    i know, but i used a Scoped injection for the authentication. because i didnt want users to use multiple tabs. with scoped, i can log out previous browser tabpages.

    but now i have a poblem that scoped instances do not stay after a force reload. because a forcereload does a new http request.

    what about sessionstorage?

    Sunday, July 26, 2020 2:59 PM
  • User475983607 posted

    i know, but i used a Scoped injection for the authentication. because i didnt want users to use multiple tabs. with scoped, i can log out previous browser tabpages.

    but now i have a poblem that scoped instances do not stay after a force reload. because a forcereload does a new http request.

    what about sessionstorage?

    Session storage is scoped to the user's browser tab and seems to fit your use case.

    Sunday, July 26, 2020 3:09 PM
  • User2110873642 posted

    if been searching for it as an option, but never figured out how to use it in blazor. or any .net core at all.

    Sunday, July 26, 2020 3:10 PM
  • User475983607 posted

    if been searching for it as an option, but never figured out how to use it in blazor. or any .net core at all.

    Blazor uses JS interop for executing JavaScript applications.

    https://docs.microsoft.com/en-us/aspnet/core/blazor/call-javascript-from-dotnet?view=aspnetcore-3.1

    The official docs has an experimental options.  The source is open so you are free to make changes if needed.

    https://docs.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-3.1#client-side-in-the-browser

    Sunday, July 26, 2020 3:19 PM