Search code examples
javascriptasp.net-mvcsignalr

How to join a client to group in MVC using signalR?


I looked everywhere but could not find or understand how to make a client join a SignalR group.
I'm using mvc 4 with the system users.

I am programming a game of online Tic-Tac-Toe. My goal is to that anytime a user will open a new game, a new group (for example: "GameRoom{Game id}" will be created. Now, whenever a new player will join the game, he will also join the game group. Then, when one of the players will make a move, the other player's browser will refresh to show the new game board. I programed it in the host side, but don't know how to do it at the client side. And one last thing - my current code currently using timer to get the data. The point is to replace it with SignalR. Here's my code:

namespace TicTacToeMVCPart2.Hubs
{
    public class GamesHub : Hub
    {
        public Task JoinGameRoom(string GameRoomName)
        {
            return Groups.Add(Context.ConnectionId, GameRoomName);
        }

        public void PlayerClick(string roomName)
        {
            Clients.OthersInGroup(roomName).OnSquareClicked();
        }
    }
}

namespace TicTacToeMVCPart2.Models
{
    // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
    public class ApplicationUser : IdentityUser
    {
        public ApplicationUser()
        {
            Games = new List<Game>();
        }

        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            return userIdentity;
        }

        public List<Game> Games { get; set; }

        public int GamesWon { get; set; }

        public int GamesLost { get; set; }

        public int GamesDraw { get; set; }
    }

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false)
        {
        }

        public DbSet<Game> Games { get; set; }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }
}

namespace TicTacToeMVCPart2.Models
{
    public class Game
    {
        public Game()
        {
        }

        public Game(string user1Id)
        {
            User1Id = user1Id;
        }

        public int GameId { get; set; }

        [Required]
        public string User1Id { get; set; }

        public string User2Id { get; set; }

        public string UserIdTurn { get; set; }

        public string WinnerId { get; set; }

        public bool IsGameOver { get; set; }

        public SquareState Square1 { get; set; }

        public SquareState Square2 { get; set; }

        public SquareState Square3 { get; set; }

        public SquareState Square4 { get; set; }

        public SquareState Square5 { get; set; }

        public SquareState Square6 { get; set; }

        public SquareState Square7 { get; set; }

        public SquareState Square8 { get; set; }

        public SquareState Square9 { get; set; }
    }
}

namespace TicTacToeMVCPart2.ViewModels
{
    public class GameModel
    {
        public Game Game { get; set; }

        public ApplicationUser User { get; set; }
    }
}

namespace TicTacToeMVCPart2.Controllers
{
    public class GamesController : Controller
    {
        ApplicationDbContext context = new ApplicationDbContext();
        private ApplicationUserManager _userManager;
        public ApplicationUserManager UserManager
        {
            get
            {
                return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
            }

            private set
            {
                _userManager = value;
            }
        }

        /// <summary>
        /// Returns the Logged User Id - defult 0
        /// </summary>
        public int LoggedUserId
        {
            get
            {
                return Utilites.LoggedUserId(Request);
            }
        }

        public ActionResult MyStats()
        {
            return View("MyStats", UserManager.FindById(User.Identity.GetUserId()));
        }

        /// <summary>
        ///  shows all Open Games of other users the logged in user can join
        /// </summary>
        /// <returns></returns>
        public PartialViewResult OpenGames()
        {
            string userId = User.Identity.GetUserId();
            var results = (
                from game in context.Games
                where game.User2Id == null && game.User1Id != userId
                select game).ToList();
            return PartialView("_allOpenGames", results);
        }

        /// <summary>
        ///  returns  "_openGameShowScreen" Partial View for a specific game
        /// </summary>
        /// <param name = "gameId"></param>
        /// <returns></returns>
        public PartialViewResult OpenGameShowScreen(int gameId)
        {
            var gameResult = GetGameById(gameId);
            ApplicationUser user = new ApplicationUser();
            if (gameResult != null)
            {
                user = UserManager.FindById(gameResult.User1Id);
            }

            GameModel model = new GameModel()
            {Game = gameResult, User = user};
            return PartialView("_openGameShowScreen", model);
        }

        /// <summary>
        /// method that allows users join games and returns the game view or message view for errors
        /// </summary>
        /// <param name = "gameId"></param>
        /// <returns></returns>
        public ActionResult UserJoinGame(int gameId)
        {
            ApplicationUser user = UserManager.FindById(User.Identity.GetUserId());
            if (user == null)
            {
                Utilites.CreateMsgCookie(Response, "Error", "Sorry, an unknown error has occurred");
                return View("Message");
            }
            else
            {
                Game gameResult = GetGameById(gameId);
                if (gameResult.User2Id != null) //game already taken
                {
                    Utilites.CreateMsgCookie(Response, "Error", "Game already being taken");
                    return View("Message");
                }
                else
                {
                    gameResult.User2Id = user.Id;
                    Random tmpRNG = new Random();
                    int tmpInt = tmpRNG.Next(2);
                    if (tmpInt == 0)
                    {
                        gameResult.UserIdTurn = gameResult.User1Id;
                    }
                    else
                    {
                        gameResult.UserIdTurn = gameResult.User2Id;
                    }

                    user.Games.Add(gameResult);
                    context.SaveChanges();
                    GameModel model = new GameModel()
                    {User = user, Game = gameResult};
                    return View("GameScreen", model);
                }
            }
        }

        /// <summary>
        /// return "ActiveGamesScreen" view with the results of ActiveGameShowResults(user) as the model
        /// </summary>
        /// <param name = "userId"></param>
        /// <returns></returns>
        public ActionResult ActiveGames()
        {
            ApplicationUser user = UserManager.FindById(User.Identity.GetUserId());
            if (user == null)
            {
                Utilites.CreateMsgCookie(Response, "Error", "Sorry, an unknown error has occurred");
                return View("Message");
            }
            else
            {
                List<Game> activeGames = ActiveGameShowResults(user);
                //ActiveGameShowResults(user);
                return View("ActiveGamesScreen", activeGames);
            }
        }

        /// <summary>
        /// return all active games of a specific user
        /// </summary>
        /// <param name = "user"></param>
        /// <returns></returns>
        private List<Game> ActiveGameShowResults(ApplicationUser user)
        {
            List<Game> results = new List<Game>();
            if (user != null)
            {
                results = context.Games.Where(x => x.IsGameOver == false && x.User2Id != null && (x.User1Id == user.Id || x.User2Id == user.Id)).ToList();
            }

            return results;
        }

        /// <summary>
        /// returns "_activeGameShowScreen" Partial View for a specific game or error in View "Message"
        /// </summary>
        /// <param name = "gameId"></param>
        /// <returns></returns>
        public ActionResult ActiveGameShowScreen(int gameId)
        {
            Game game = GetGameById(gameId);
            if (game == null)
            {
                Utilites.CreateMsgCookie(Response, "Error", "Sorry, an unknown error has occurred");
                return View("Message");
            }
            else
            {
                string userId = User.Identity.GetUserId();
                //Get rival user Id
                if (game.User1Id == userId)
                {
                    userId = game.User2Id;
                }
                else
                {
                    userId = game.User1Id;
                }

                ApplicationUser user = UserManager.FindById(userId);
                GameModel model = new GameModel()
                {Game = game, User = user};
                return PartialView("_activeGameShowScreen", model);
            }
        }

        /// <summary>
        /// get game from context by gameId , Defult result - null
        /// </summary>
        /// <param name = "gameId"></param>
        /// <returns></returns>
        private Game GetGameById(int gameId)
        {
            Game gameResult = (
                from game in context.Games
                where game.GameId == gameId
                select game).FirstOrDefault();
            return gameResult;
        }

        /// <summary>
        /// method to create new gamrs, returns "GameScreen"  View or error by message view
        /// </summary>
        /// <returns></returns>
        public ViewResult CreateNewGame()
        {
            var user = UserManager.FindById(User.Identity.GetUserId());
            if (user == null)
            {
                Utilites.CreateMsgCookie(Response, "Error", "Sorry, an unknown error has occurred");
                return View("Message");
            }
            else
            {
                Game game = new Game();
                game.User1Id = user.Id;
                user.Games.Add(game);
                context.Games.Add(game);
                context.SaveChanges();
                GameModel model = new GameModel{Game = game, User = user};
                return View("GameScreen", model);
            }
        }

        /// <summary>
        /// returns GameScreen View by gameId or error by message view 
        /// </summary>
        /// <param name = "gameId"></param>
        /// <returns></returns>
        public ViewResult GoToGameScreen(int gameId)
        {
            var user = UserManager.FindById(User.Identity.GetUserId());
            if (user == null)
            {
                Utilites.CreateMsgCookie(Response, "Error", "Sorry, an unknown error has occurred");
                return View("Message");
            }
            else
            {
                Game game = GetGameById(gameId);
                GameModel model = new GameModel{Game = game, User = user};
                return View("GameScreen", model);
            }
        }
    }
}

and here's client side related code:

namespace TicTacToeMVCPart2.Controllers
{
    public class GamesApiController : ApiController
    {
        ApplicationDbContext context = new ApplicationDbContext();
        private ApplicationUserManager _userManager;
        public IEnumerable<ApplicationUser> Get()
        {
            return context.Users;
        }

        public ApplicationUserManager UserManager
        {
            get
            {
                //(System.Web.HttpContext.Current)//lock (System.Web.HttpContext.Current)
                //{
                return _userManager ?? System.Web.HttpContext.Current.Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
            //}
            }

            private set
            {
                _userManager = value;
            }
        }

#region Methods
        /// <summary>
        /// update the server data by reciving the model and square and returns the new model
        /// </summary>
        /// <param name = "_model"></param>
        /// <param name = "squareId"></param>
        /// <returns></returns>
         //square clicked via post
        [Route("api/gamesapi/{squareId}")]
        public HttpResponseMessage Post([FromBody] GameModel model, int squareId)
        {
            HttpResponseMessage response;
            if (model == null)
            {
                //Utilites.CreateMsgCookie(Response, "Error", "Sorry, an unknown error has occurred");
                response = Request.CreateErrorResponse(HttpStatusCode.NotFound, "model wasn't found");
                return response;
            }

            //GameModel model = JsonConvert.DeserializeObject<GameModel>(_model);
            Game game = GetGameById(model.Game.GameId);
            if (game == null)
            {
                response = Request.CreateErrorResponse(HttpStatusCode.NotFound, "game wasn't found");
            }
            else
            {
                if (game.UserIdTurn == game.User1Id) //pressing user is user1
                {
                    ChangeSquareState(game, squareId, true);
                    game.UserIdTurn = game.User2Id;
                }
                else //game.UserIdTurn == game.User2Id - pressing user is user2
                {
                    ChangeSquareState(game, squareId, false);
                    game.UserIdTurn = game.User1Id;
                }

                SquareState[] board = new SquareState[]{game.Square1, game.Square2, game.Square3, game.Square4, game.Square5, game.Square6, game.Square7, game.Square8, game.Square9};
                if (didPlayerWin(board))
                {
                    game.WinnerId = model.User.Id;
                    UpdateUserGameState(1, game.User1Id);
                    UpdateUserGameState(2, game.User2Id);
                    game.IsGameOver = true;
                }
                else
                {
                    bool isBoardFull = true;
                    for (int i = 0; i < board.Length; i++)
                    {
                        if (board[i] == SquareState.Blank)
                        {
                            isBoardFull = false;
                            break;
                        }
                    }

                    if (isBoardFull)
                    {
                        UpdateUserGameState(3, game.User1Id);
                        UpdateUserGameState(3, game.User2Id);
                        game.IsGameOver = true;
                    }
                }

                context.SaveChanges();
                response = Request.CreateResponse(HttpStatusCode.OK, game);
            }

            return response;
        }

        /// <summary>
        /// When a game is over, recive a gameState and update the user. 1 for a win, 2 for loss, 3 for aa draw
        /// </summary>
        /// <param name = "gameState"></param>
        private void UpdateUserGameState(int gameState, string userId)
        {
            var user = UserManager.FindById(userId);
            switch (gameState)
            {
                case 1:
                    user.GamesWon++;
                    break;
                case 2:
                    user.GamesLost++;
                    break;
                case 3:
                    user.GamesDraw++;
                    break;
                default:
                    break;
            }

            UserManager.UpdateAsync(user);
        }

        [HttpGet]
        [Route("api/gamesapi/{gameId}")]
        /// <summary>
        /// method to bring the latest game's state from the context and send it back in a GameModel
        /// </summary>
        /// <param name = "_model"></param>
        /// <returns></returns>
        public HttpResponseMessage Get(int gameId)
        {
        }

        /// <summary>
        /// method that check if the board have a line(3 squars in a row)
        /// of the same  element , defult - returns fault
        /// </summary>
        /// <param name = "board"></param>
        /// <returns></returns>
        private bool didPlayerWin(SquareState[] board)
        {
        }

        /// <summary>
        /// change the SquareState of a specific square of the sended game according to the pressing user
        /// </summary>
        /// <param name = "game"></param>
        /// <param name = "SquareId"></param>
        /// <param name = "_isUser1"></param>
        private void ChangeSquareState(Game game, int SquareId, bool _isUser1)
        {
        }

        /// <summary>
        /// get game from context by gameId , Defult result - null
        /// </summary>
        /// <param name = "gameId"></param>
        /// <returns></returns>
        private Game GetGameById(int gameId)
        {
            Game gameResult = (
                from game in context.Games
                where game.GameId == gameId
                select game).FirstOrDefault();
            return gameResult;
        }
#endregion
    }
}

GameScreen.cshtml:

    @model TicTacToeMVCPart2.ViewModels.GameModel

@{
    ViewBag.Title = "GameScreen";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<script>
    var game_model = @Html.Raw(Json.Encode(@Model));
</script>
<h2>GameScreen</h2>
<div id="waiting">


    @if (Model.Game.User2Id == null)
    {
        <h4 id="waitingMsg">Waiting for another player to join the game</h4>
    }
</div>


<div id="TurnTitle"></div>
<br />
<div id="gameBoard">

</div>


@section  Scripts {

    <script src="~/Scripts/TicTacToeScript.js"></script>
}

var tableDiv;
var board;
var timer;
var hub;
var con;


function UpdateTurnTitle() {
    var div = document.getElementById("TurnTitle");
    if (div) {
        if (game_model.Game.UserIdTurn === game_model.User.Id) {
            div.innerHTML = "Your Turn";
        }
        else {
            div.innerHTML = "opponent Turn";
        }
    }
}


function convertGameStateValueToEnum(val) { // function that helps understand the class names
    var res;
    switch (val) {
        case 0:
            res = "Blank";
            break;
        case 1:
            res = "User1";
            break;
        case 2:
            res = "User2";
            break;

    }
    return res;
}


$(document).ready(
(function () {
    con = $.hubConnection();
    hub = con.createHubProxy('GamesHub');

    hub.on('OnSquareClicked', RefreshGame());

    con.start();

    


    if (game_model.Game.User2Id != null) {
        UpdateTurnTitle();
    }
    tableDiv = document.getElementById("gameBoard");
    FillArray();
    if (tableDiv) { // creating the Tic-Tac-Toe table for the first time the page is loaded
        var counter = 1;
        for (var i = 0; i < 3; i++) {
            var rowDiv = document.createElement("div");
            rowDiv.className = "row";
            for (var j = 0; j < 3; j++) {
                var colDiv = document.createElement("div");
                colDiv.id = counter;
                var partOfClassName = convertGameStateValueToEnum(board[counter - 1]);
                colDiv.className = 'col-sm-4 TicTac-block ' + partOfClassName;

                if (partOfClassName == 'Blank') { // add Event Listener for blank squars
                    colDiv.addEventListener("click", click, false);
                }

                counter++;
                rowDiv.appendChild(colDiv);

            }
            tableDiv.appendChild(rowDiv);
           

        }
        timer = setInterval(function () { RefreshGame(); }, 1000);
    }
}())
);

function RefreshTable() {
    FillArray();

    for (var i = 0; i < board.length; i++) {
        //var div = $('#' + (i + 1));
        var div = document.getElementById((i + 1).toString());
        var partOfClassName = convertGameStateValueToEnum(board[i]);
        div.className = 'col-sm-4 TicTac-block ' + partOfClassName;
        if (partOfClassName != 'Blank') {
            div.removeEventListener("click", click, false);
        }

    }
}

function FillArray() { //filling the board by using game_model.Game Squares. should be done after refreshing data and before
    // RefreshTable functions
    board = [
         game_model.Game.Square1, game_model.Game.Square2, game_model.Game.Square3, game_model.Game.Square4,
        game_model.Game.Square5, game_model.Game.Square6, game_model.Game.Square7, game_model.Game.Square8, game_model.Game.Square9
    ];
}

function click() { // happends when one square of the div board has been clicked
    if (game_model.Game.User2Id == 0) {
        alert("Waiting for another player to join the game");
        return;
    }
    if (game_model.Game.UserIdTurn != game_model.User.Id) {
        alert("It's not your turn yet");
        return;
    }
    var div = document.getElementById(this.id);
    RefreshGameAfterClick(div);
}

function RefreshGame() { //timer function
    RefreshData();

    if (game_model.Game.User2Id != null) {
        var divChild = document.getElementById('waitingMsg'); //remove waitingMsg div if exist when there are two players
        if (divChild) {
            var divFather = document.getElementById('waiting');
            divFather.removeChild(divChild);
        }


        RefreshTable();

        if (game_model.Game.IsGameOver) {
            GameOver();
        }

        else {
            UpdateTurnTitle();

        }

    }



}

// commiting GameOver functions
function GameOver() {
    clearInterval(timer);
    //updating Title  by checking the results 
    if (game_model.Game.WinnerId == null) {
        var divTitle = document.getElementById('TurnTitle');
        if (divTitle) {
            divTitle.innerHTML = 'Game Over - Draw';
        }
    }

    else if (game_model.Game.WinnerId == game_model.User.Id) {
        var divTitle = document.getElementById('TurnTitle');
        if (divTitle) {
            divTitle.innerHTML = 'Game Over - You won';
        }
    }
    else {
        var divTitle = document.getElementById('TurnTitle');
        if (divTitle) {
            divTitle.innerHTML = 'Game Over - You Lost';
        }
    }
    DisableAllClicks();
}

function DisableAllClicks() // disabling all the Event Listeners of the game board divs
{
    for (var i = 0; i < board.length; i++) {
        var div = document.getElementById((i + 1).toString());
        div.removeEventListener("click", click, false);
    }
}


function RefreshGameAfterClick(div) {
    RefreshDataAfterClick(div);

    if (game_model.Game.IsGameOver) { // if game over, active GameOver method
        GameOver();
    }

    else {
        UpdateTurnTitle();

    }

}



function RefreshDataAfterClick(div) { // sends post to server and updating game_model.Game variable
    $.ajax({
        type: "POST",
        url: '/api/GamesApi/' + div.id,
        dataType: 'json',
        contentType: 'application/json; charset=utf-8',
        data: JSON.stringify(game_model),
        success: function (data) {
            if (data) {
                game_model.Game = data;
            }
        }
    })
}

function RefreshData() { // timer function - GET type
    $.ajax({
        type: "GET",
        url: '/api/GamesApi/' + game_model.Game.GameId,
        dataType: 'json',
        contentType: 'application/json; charset=utf-8',
        success: function (data) {
            if (data) {
                game_model.Game =data;
            }
        }
    })
}


Solution

  • I found the problem. my javascript had(I just also noticed the javascript I wrote here was a previous version):

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
    var hub = $.Connection.gamesHub;

    and the right code is $.connection.gamesHub with small case c.