dsuryd / dotNetify

Simple, lightweight, yet powerful way to build real-time web apps.
https://dotnetify.net
Other
1.17k stars 164 forks source link

Group chat room example #131

Closed bugged84 closed 5 years ago

bugged84 commented 6 years ago

I've been playing around with MuticastVM and I'm curious about partitioned instances. Your docs give an example of using the user identity as a group name to allow a single user to share a VM instance across multiple devices.

What I would like to do is be able to route to a partitioned VM instance. For example, consider your chat room example. Perhaps I have a list of chat rooms displayed and a user can click on a link to one of the rooms and join that conversation. Or a user might share a link to a specific chat room with a coworker via email. The coworker should be able to click the link and connect to the partitioned VM instance specified in the link.

Is this possible? I've started looking through your code base for ideas because I haven't found any way to do this from the docs or from the examples, but maybe I missed something.

I was hoping that maybe when configuring a RouteTemplate I could set the UrlPattern property to something like "chat/{id}" and then define an id parameter on the constructor of the ChatRoomVM in order to be passed the room identifier for a requested URL such as "chat/5". Then the view model could use this value to generate the group name for partitioning.

dsuryd commented 6 years ago

I have no example for this as the multicast feature is a pretty recent addition, but I think you're on the right track. We can either use the onRouted event, or passing vmArg to initialize the view model's group name.

bugged84 commented 6 years ago

@dsuryd I started with your SPA template which has a Dashboard view model that also implements IRoutable. I added the following code at beginning of the constructor.

this.OnRouted(
   (sender, e) =>
   {
      var from = e.From;
   }
);

However, a breakpoint set on that assignment statement never gets hit when I run the application. Is there something else I'm missing?

dsuryd commented 6 years ago

Sorry, it doesn't work with the dotNetify-Elements SPA template - it's a bug, the necessary props are not getting passed to the server when using VMContext element. Instead, start with the demo SPA at https://github.com/dsuryd/dotNetify/tree/master/Demo/React/SPA. Implement the IRoutable, and it should work.

bugged84 commented 5 years ago

@dsuryd with the demo you mentioned above, the OnRouted action is now working for me.

Consider the view model below. If I remove the var m_groupName = e.From; from the OnRouted action, then every React component that connects to this view model displays the same date/time from the Name property as expected for a multicast view model.

However, if I add the var m_groupName = e.From; to the OnRouted action in order to partition the view model by chat room ID (e.g. "/5"), then every React component that connects to this view model displays a different date/time rather than a single date/time per chat room ID.

It doesn't seem like the GroupName property is behaving properly. Am I doing something wrong, or is this a bug?

public class ChatRoomVM : MulticastVM, IRoutable
{
    private string m_groupName;

    public override string GroupName => m_groupName;

    public string Name = DateTime.Now.ToLongTimeString();

    public ChatRoomVM()
    {
        this.OnRouted(
           (sender, e) =>
           {
              var m_groupName = e.From;
           }
        );
    }
}
bugged84 commented 5 years ago

I came up with a solution using vmArg from the JSON data passed on the DotNetifyHubContext. Using middleware to access this context before a view model is instantiated, the group name is computed from the route data available in vmArg. The middeware then stores the group name on the Items dictionary of the HubCallerContext also available on the DotNetifyHubContext. Finally, the view model uses a dependency on IHubCallerContextAccessor to override the GroupName property inherited from MulticastVM.

Here are some helper methods for interacting with the HubCallerContext.

public static class HubCallerContextExtensions
{
   private static readonly string s_viewModelGroupNameKey
      = $"ViewModelGroupName_{Guid.NewGuid()}";

   public static string GetViewModelGroupName(this HubCallerContext context)
   {
      return context.Items.ContainsKey(s_viewModelGroupNameKey)
         ? (string)context.Items[s_viewModelGroupNameKey]
         : null;
   }

   public static void SetViewModelGroupName(
      this HubCallerContext context
    , string value
   )
   {
      context.Items[s_viewModelGroupNameKey] = value;
   }
}

Here is the middleware.

public class GroupNameMiddleware : IMiddleware
{
   #region IMiddleware Methods

   public Task Invoke(DotNetifyHubContext context, NextDelegate next)
   {
      if (context.Data is JObject data)
      {
         context
           .CallerContext
           .SetViewModelGroupName(GetRoute(data));
      }

      return next(context);
   }

   #endregion

   private static string GetRoute(JObject data)
   {
      return
         data
           .Properties()
           .FirstOrDefault(p => p.Name.StartsWith("RoutingState"))
          ?.Value
           .ToString();
   }

Here is the updated ChatRoomVM.

public class ChatRoomVM : MulticastVM, IRoutable
{
    private readonly IHubCallerContextAccessor m_hubCallerContextAccessor;

    public RoutingState RoutingState { get; set; }

    public override string GroupName =>
        m_hubCallerContextAccessor
            .CallerContext
            .GetViewModelGroupName();

    public string Name = DateTime.Now.ToLongTimeString();

    public ChatRoomVM(IHubCallerContextAccessor hubCallerContextAccessor)
    {
        this.OnRouted(
           (sender, e) =>
           {
              // handle user entry
           }
        );
    }
}

This solution seems to work fine for partitioning the view model into multiple chat rooms. The OnRouted action can be used to notify current users when someone new routes into the chat room.

dsuryd commented 5 years ago

Looks good; couldn't have done it better myself!

davidgoh commented 2 years ago

Hi @dsuryd would like to check with you on the following issues we discovered with this code routing code example

We've discovered when using react hooks [useConnect("ChatRoomVM",...)] to connect to the VM, and putting a breakpoint on the Break Point here part, the execution will not reach the VM's constructor.

Instead this is working fine when using the conventional way to connect: dotnetify.react.connect('ChatRoomVM ', this);.

public class ChatRoomVM : MulticastVM, IRoutable
{

    public ChatRoomVM(IHubCallerContextAccessor hubCallerContextAccessor)
    {
       // ** Break Point here ** 
        this.OnRouted(
           (sender, e) =>
           {
              // handle user entry
           }
        );
    }
}

Is there any way to make react hooks to work with routing?

dsuryd commented 2 years ago

@davidgoh Please open a new issue instead of adding to a closed one.

UseConnect hook works well with routing, see working demo: https://github.com/dsuryd/dotNetify/tree/master/Demo/React/LazyLoadRouting