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;
}
}
})
}
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.