protected static SavedGameDto ConvertFromXml(SavedGameXML game)
        {
            if (game == null)
            {
                return(null);
            }

            if (!(game.CellTypes?.Count >= 3))
            {
                throw new ArgumentOutOfRangeException(nameof(game) + "." + nameof(game.CellTypes), "Не может быть меньше трёх");
            }

            HashSet <CellTypeDto> types = game.CellTypes
                                          .Select(xml => CellTypeDto.Create(xml.Id, xml.Value))
                                          .ToHashSet();

            var tps = types.ToDictionary(tp => tp.Id);

            HashSet <CellDto> cells = null;

            if (game.Cells != null)
            {
                cells = new HashSet <CellDto>();
                foreach (CellXML cell in game.Cells)
                {
                    if (types.FirstOrDefault(dto => dto.Id == cell.TypeId) == null)
                    {
                        throw new ArgumentException("Такого типа нет в списке", "cell.id");
                    }
                    cells.Add(new CellDto(cell.Id, cell.Row, cell.Column, tps[cell.TypeId]));
                }
            }

            if (!(game.Users?.Count > 0))
            {
                throw new ArgumentOutOfRangeException(nameof(game) + "." + nameof(game.Users), "Не может быть меньше одного");
            }
            HashSet <UserDto> users = game.Users
                                      .Select(xml => new UserDto(xml.Id, xml.Name, xml.ImageIndex, xml.Turn, xml.Id == game.CurrentUser.UserId, tps[xml.TypeId]))
                                      .ToHashSet();

            return(new SavedGameDto
                   (
                       users,
                       cells,
                       types,
                       game.Game.Rows,
                       game.Game.Columns,
                       game.Game.LengthToWin
                   ));
        }
        //private int ShiftForCalculateCompleteLine;//сдвиг относительно проверяемой ячейки
        //public event NotifyChangedCellHandler ChangedCellEvent;

        /// <summary>Изменение типа содержания заданной ячейки</summary>
        /// <param name="cell">Заданная ячейка</param>
        /// <param name="type">Новый тип содержания</param>
        void SetCellType(CellDto cell, CellTypeDto type)
        {
            // Если текущий тип равен присваиваемому, то ничего не делается.
            // Можно написать сокращённо:
            //if (Cells[cell.Row, cell.Column]?.CellType == type)
            if (Cells[cell.Row, cell.Column] != null && Cells[cell.Row, cell.Column].CellType == type)
            {
                return;
            }

            // В противном случае создаётся новый экземпляр ячейки с новым значением контента.
            // Если ячейка заполнена, то Id берётся из неё. Если не заполнена, то из переданного
            // параметра cell. После присвоения ячейке нового экземпляра, создаётся событие
            // с передачей нового содержания ячейки.
            ChangedStateEvent?.Invoke(this, new ChangedStateHandlerArgs(NamesState.CellType,
                                                                        Cells[cell.Row, cell.Column] = new CellDto(Cells[cell.Row, cell.Column]?.Id ?? cell.Id, cell.Row, cell.Column, type)));
        }