Procedura dettagliata: creazione di un gioco via Internet con F#, C# e Windows Azure

Rivedendo questo argomento e l'esempio che accompagna lo, verrà illustrato come creare un'applicazione web (in questo caso, un gioco di parole) tramite F#, il c, il model-view-controller) (MVC) ASP.NET, Windows Azure e JavaScript.In questo argomento vengono illustrate numerose tecniche di codifica utili spiegando vengono estratti di codice nell'esempio di WordGrid.L'esempio di WordGrid è un servizio cloud di Windows Azure che è possibile compilare ed eseguita localmente o distribuire in Windows Azure.Un servizio cloud di Windows Azure è semplicemente un'applicazione web in esecuzione in Windows Azure.Poiché WordGrid è un servizio cloud, di giocatori possono accedervi da parte del mondo tramite i rispettivi browser.

Seguendo questa procedura dettagliata e studiando il codice che illustri, verrà:

  • Come integrare F# e c (e JavaScript per il client in un'applicazione web in esecuzione in cloud.

  • Come utilizzare le code di servizi del servizio di archiviazione Windows Azure per comunicare tra i ruoli in un servizio cloud di Windows Azure.

  • Come utilizzare ASP.NET MVC 4 e il motore di visualizzazione Razor per compilare una pagina Web che riflette dati dinamici.

  • Come utilizzare i provider e le espressioni di query dei tipi F# per accedere e modificare i dati.

Prerequisiti

Se si desidera seguire questa procedura dettagliata, installare l'applicazione di esempio WordGrid.Vedere Procedura dettagliata: pubblicazione di un'applicazione MVC F#/C# MVC in Windows Azure per iniziare.

In questa procedura dettagliata si presuppone che sia installato l'esempio, collegato a un database e sia stato passato almeno una volta.In questa procedura dettagliata si presuppone inoltre che l'utente abbia familiarità con il linguaggio c e F# e creato almeno uno o due progetti Windows Azure.Se non è stato eseguito alcun lavoro con Windows Azure, è possibile richiederne una relazione gratuita su Windows Azure.Per apprendere i concetti di base di developmen cloud di servizio, vedere le procedure dettagliate come Getting Started with the Windows Azure Tools for Visual Studio e Sviluppare e implementare i servizi di cloud Windows Azure utilizzando Visual Studio.

Sommario

Di seguito sono elencate le diverse sezioni di questo argomento.

  • Panoramica del codice del gioco F#

  • Cenni preliminari sul codice c ASP.NET

  • Cenni preliminari sul ruolo di lavoro F#

Panoramica del codice del gioco F#

Come mostrato nell'esempio di WordGrid, è possibile integrare F# con un'applicazione MVC ASP.NET in una coppia di metodi.È possibile separare il codice nelle classi, visualizzazioni e nelle classi controller di modello tramite il modello di applicazione MVC.Le classi di modello contiene il codice comune di c e gli oggetti astratti che sono indipendenti dall'interfaccia utente.Specificare il layout delle pagine nelle classi di visualizzazione.Nelle classi controller, specificare quali richieste HTTP vengono elaborate e gestite, che include corrispondere i dati di modello appropriati alla visualizzazione appropriata, in base ai dettagli di ogni richiesta HTTP.

In questo esempio, creare l'interfaccia utente per le pagine Web tramite il motore di rendering Razor, che fa parte di ASP.NET MVC 4.Dopo la sintassi del motore Razor, si scrive il codice c nelle pagine di visualizzazione.

In questa applicazione, F# immette immagine in due modi.Innanzitutto, si utilizza F# come raccolta che si fa riferimento il codice c.Quindi si utilizza F# come ruolo separato di lavoro che elabora le richieste di cercare parole in un dizionario.

Nella sezione successiva, rivedrete il codice rappresentativo per ogni sezione a sua volta e quindi verrà modificato il codice per aggiornare lo stile di codifica.Innanzitutto, individuare la libreria F#, ovvero il progetto WordGridGame nella soluzione.

Questo codice F# astrae il gioco di parole in modo normale per la programmazione orientata a oggetti.Il codice è costituito da diversi tipi di classe che rappresentano elementi di base del gioco di parole, la sezione, il giocatore, la scheda del gioco e del gioco stesso.

Ad esempio, si consideri la classe della sezione.

// Represents a WordGrid tile.
[<AllowNullLiteral>]
type Tile(letter : Letter, blankLetter : Letter option) =

    // For a played tile, the second parameter can be a letter when the first parameter is Letter.Blank
    static let letterChar = 
       [| '_'; 'A'; 'B'; 'C'; 'D'; 'E'; 'F'; 'G'; 'H'; 'I'; 'J'; 'K'; 'L'; 'M'; 'N'; 'O'; 'P'; 'Q'; 'R'; 'S'; 'T'; 'U'; 'V'; 'W'; 'X'; 'Y'; 'Z' |]

    static let pointValues =
       [| 0; 1; 3; 4; 2; 1; 4; 2; 4; 1; 8; 5; 1; 3; 1; 1; 3; 10; 1; 1; 1; 1; 5; 4; 8; 4; 10 |]

    let letterValue = letter

    let pointValue = 
        pointValues.[(int) letterValue]

    member this.LetterChar = letterChar.[int letterValue]
    member this.LetterValue = letterValue
    member this.PointValue = pointValue        
    member val BlankLetter : Letter option = blankLetter with get, set

    static member FromString(tiles : string) =
        Seq.map(fun (elem : char) -> new Tile(elem)) tiles
        |> Seq.toList

    new(letter : Letter) =
        new Tile(letter, None)

    new(ch : char) =
        if (ch =  '_') then Tile(Letter.Blank) else
            if (ch >= 'a' && ch <= 'z') then
                // This represents a blank tile that has been played as a letter.
                Tile( Letter.Blank, Some(enum<Letter> (int ch - int 'a' + 1 )))
            else
                Tile( enum<Letter> (int ch - int 'A' + 1))

Nel codice seguente:

  • L'attributo AllowNullLiteral viene utilizzato il tipo di Tile.È utile memorizzare gli oggetti di Tile durante l'interazione con il c e utilizzare le classi .NET per le raccolte generiche.

  • In F#, gli argomenti del costruttore primario vengono visualizzati dopo il nome della classe.In questo caso, viene creata una sezione specifica un valore di Letter, che rappresenta un'enumerazione dei tipi possibili la sezione, inclusi gli eventuali caratteri alfabetici e spazio vuoto.Nel gioco di parole, i giocatori possono utilizzare una sezione vuota per rappresentare una lettera.Quando questa sezione è in scaffale la sezione di un lettore, la sezione non ha alcun valore della lettera.Quando il giocatore inserisce la sezione della scheda del gioco, la sezione ottiene un valore specifico della lettera.Il tipo di opzione F# viene utilizzata per modellare questo comportamento, ma il tipo opzione non è facile da utilizzare da c, poiché il tipo di opzione viene visualizzato come istanza di FSharpOption in c.Il punto successivo contiene una soluzione al problema.

  • La classe include inoltre costruttori aggiuntivi, che utilizzano la nuova parola chiave.Questi costruttori devono chiamare il costruttore primario, ma possono anche eseguire calcoli aggiuntivi.Il costruttore che accetta Letter come singolo parametro consente semplicemente di omettere il parametro che gestisce gli spazi vuoti.Tramite questo costruttore, non è necessario utilizzare il tipo di opzione da c.Il costruttore che accetta un carattere viene chiamato dal metodo statico di FromString, che supporta la rappresentazione delle sezioni o della scheda del gioco come stringa che può essere archiviato nel database.In questa codifica, una lettera maiuscola indica una sezione comune della lettera, una minuscola rappresenta uno spazio vuoto che è stato riprodotti come lettera specifica e il carattere di sottolineatura rappresenta una sezione vuota unplayed.

    La classe utilizza i campi non statiche e statici, che sono privati ed espone proprietà pubbliche per ottenere il valore della lettera e il valore del punto della sezione.

  • Utilizzare le parole chiave member val per definire BlankLetter come proprietà automatica in F#.La proprietà di BlankLetter rappresenta la lettera che una sezione vuota è stata fornita durante il gioco.

La classe di Player è una classe tipica i dati del database degli specchi.Il codice seguente viene illustrata la classe di Player, con alcune funzioni omessi per brevità.

// Represents a player in an active game and manages the state for that player,
// including the score and tile rack.
type Player(userId, gameId, playerId, name, score, tiles) =

    // The userId that the player uses to log in.
    member val UserId = userId

    // The database Id of the game
    member val GameId = gameId

    // The database ID of the player
    member val PlayerId = playerId

    // The list of tiles in the player's rack
    member val Tiles = tiles with get, set

    // The player's score.
    member val Score = score with get, set

    // The name of the player.
    member val Name = name

    // Returns the tiles as a System.Collections.Generic.List instead of an F# list.
    member this.GetTilesAsList() =
        let returnList = new System.Collections.Generic.List<Tile>()
        for tile in this.Tiles do
            returnList.Add(tile);
        returnList

    // Sets the player's tiles from a System.Collections.Generic.List.
    member this.TilesFromList(tileList : System.Collections.Generic.List<_>) =
        this.Tiles <- List.ofSeq tileList        

    // This constructor is used when existing players start a game

.
    new(gameId, playerId) =
        // Get the player from the database
        let dataContext = sqldata.GetDataContext()
        let player =
            query { 
                for player in dataContext.Players do
                where (player.Id = playerId)
                select player
            }
            |> Seq.exactlyOne

        let playerStateResults =
            query {
                for playerState in dataContext.PlayerState do
                where (playerState.GameID = gameId && playerState.PlayerID = playerId)
                select playerState
                }
            |> Seq.toList

        match playerStateResults with
        | [] -> Player(player.UserId, gameId, player.Id, player.UserName, 0, List.empty)
        | [ playerState ] -> Player(player.UserId, gameId, player.Id, player.UserName, playerState.Score, Tile.FromString (playerState.Tiles))
        | _ -> raise (new System.InvalidOperationException()); Player(0, 0, 0, "", 0, List.empty)

    // This constructor is used when a new player starts a game. If the player has
    // played a previous game, the database contains a record for them. If not, a new one is created.
    static member FindOrCreate(userId, userName) : Player =

        // Look up the player, and create a record if the player doesn't already exist in the database.
        // Get the player from the database
        let dataContext = sqldata.GetDataContext()
        let players =
            query { 
                for player in dataContext.Players do
                where (player.UserId = userId && player.UserName = userName)
                select player
            }
            |> Seq.toList
        match players with
        | [] -> // no player was found, so create
                let sqlText = System.String.Format("INSERT INTO Players VALUES ('{0}', '{1}')", userName, userId)
                dataContext.DataContext.ExecuteCommand(sqlText) |> ignore
                Player.FindOrCreate(userId, userName)
        | [ player ] -> // one player was found
                Player(userId, 0, player.Id, userName, 0, List.empty)
        | _ -> // multiple players found: error case
                raise (new System.Exception("Duplicate player found."))

    // Gets all the games that the player is currently playing by their IDs.
    member this.GetGameIDs(myTurnOnly : bool) =
        let dataContext = sqldata.GetDataContext()
        if (myTurnOnly) then
            query {
                for game in dataContext.Games do
                join gamePlayer in dataContext.GamePlayers on (game.Id = gamePlayer.GameId)
                where (game.GameState <> int GameState.GameOver &&
                       gamePlayer.PlayerId = this.PlayerId &&
                       gamePlayer.Position =? game.CurrentPlayerPosition)
                select (game.Id)
                }
                |> Seq.toArray
        else
            query {
                for game in dataContext.Games do
                join gamePlayer in dataContext.GamePlayers on (game.Id = gamePlayer.GameId)
                where (game.GameState <> int GameState.GameOver &&
                       gamePlayer.PlayerId = this.PlayerId )
                select (game.Id)
                }
                |> Seq.toArray

Le istanze della classe di Player vengono create quando una pagina Web di WordGrid è necessaria.Questo comportamento è dovuto il gioco viene implementato da un'applicazione web che utilizza il modello REST (trasferimento di representational state transfer).Il REST è un modello di progettazione, non una tecnologia.In progettazione basata su resto include il principio che ogni richiesta Web (ad esempio un giocatore che visualizza un gioco specifico o che invia un movimento) è indipendente e il server non consente alcune informazioni sullo stato.Pertanto, tutti gli oggetti vengono ricreati per ogni nuova richiesta.

Si notino le seguenti informazioni sul codice precedente:

  • Il metodo di GetTilesAsList illustrata la conversione di un elenco di F# in List<T>.Questo tipo di conversione è qui utilizzato per evitare di dover utilizzare l'elenco di F# in c.Per lunghi elenchi in cui le prestazioni sono un problema, è possibile utilizzare l'elenco c nel codice F# per evitare scorrere l'elenco più necessario.

  • Il codice del database nel gioco di WordGrid completamente è basato sul provider del tipo di SqlDataConnection, creato utilizzando una sola riga di codice all'inizio del file di codice di WordGrid.fs.

    type sqldata = SqlDataConnection<ConnectionStringName = "DefaultConnection", ConfigFile = @"Web.config">
    

    Questa singola riga di codice indica al compilatore F# genera un insieme di tipi che rappresentano oggetti di database.Il database fa riferimento la stringa di connessione memorizzata come DefaultConnection nel file Web.config del progetto.Altrove in questo file, è sufficiente accedere alla proprietà di DataContext del provider del tipo di sqldata per accedere ai tipi generati provider del tipo, quali giocatori e giochi.I tipi di queste proprietà sono annidati, vengono generati dalle tabelle di database con lo stesso nome e le relative istanze sono raccolte di righe da tali tabelle.La proprietà di DataContext fornisce un'istanza di una classe generata derivata da DataContext.

  • Utilizzare le espressioni di query per accedere alle tabelle di database.Nel metodo di GetGameIDs (e il metodo di GetGameNames, non viene visualizzato), join unisce le tabelle di GamePlayers e di Game.

  • Nel metodo di FindOrCreate, il metodo di ExecuteCommand esegue un comando costruito Transact-SQL inserire un nuovo record per il giocatore nel database.

  • Se si parte informazioni su F#, si noti l'utilizzo dell'espressione corrispondente creare un ramo in modo appropriato il codice basato su se la raccolta di giocatori è 0, 1, o più elementi.Vedere Espressioni match (F#).

I seguenti tipi di WordGrid.fs nella scheda del gioco, che in questo argomento non sono inclusi in quanto è lungo e non mostrati alcuni concetti aggiuntivi.

I seguenti tipi di WordGrid.fs sono il tipo di Move.Il tipo rappresenta un singolo gioco, ovvero, la posizione di uno o più sezione righe e colonne della scheda in particolare per formare una o più parole.Questo tipo include le sezioni che vengono riprodotte, la posizione delle sezioni nella scheda, la direzione di gioco e le sezioni che rimangono nello scaffale del giocatore.Queste proprietà sono tutte le proprietà automatiche.

// Represents the play for a single turn in a WordGrid game.
type Move(gameId : int, playerId : int, row : int, col : int,
          dir : Direction, tiles : string, remainingTiles : string) =
      // The id from the database for the game in which this move was played.
      member val GameID = gameId

      // The player who's making this move.
      member val PlayerID = playerId

      // The row of the first tile that's played in this move.
      member val Row = row

      // The col of the first tile that's played in this move.
      member val Col = col

      // The direction of the move.
      member val Direction = dir

      // The tiles that were played for this move, not including any previously
      // played tiles that are part of the word or words that the move formed.
      member val Tiles = Tile.FromString(tiles)

      // The tiles that remain in the user's rack after the move.
      member val RemainingTiles = Tile.FromString(remainingTiles)

Le informazioni sullo spostamento sono costituiti nel client del giocatore, nel browser.JavaScript nell'applicazione web consente il giocatore le sezioni sulla scheda, in modo che non è la parte del codice F# affatto.Quando il giocatore ha specificato un movimento e ha scelto il pulsante Invia Play, un form viene inviato come richiesta HTTP POST.Un controller, denominato Board nell'applicazione ASP.NET, indica i valori del form che vengono inviati e generano un oggetto di Move, che viene quindi inviato alla classe Game.La classe Game include la logica per avviare un gioco, il carico del gioco dal database, elaborare i movimenti e determinare quando il gioco completa.Questa classe è lunga, pertanto evidenziazioni di questo argomento alcuni aspetti chiave anziché includere il codice completo.È necessario rivedere il codice completo nell'esempio per comprendere come tutte adatta raccolta.

Nel metodo di ProcessPlay, in codice come stored procedure utilizzando il provider del tipo.

// Update the database tables: Games, PlayerState, Plays
Game.DataContext.SpUpdateGame(Util.nullable this.GameId,
                              Util.nullable player.PlayerId,
                              Util.nullable this.MoveCount,
                              Game.AsString player.Tiles,
                              mainWord,
                              gameBoard.AsString,
                              Game.AsString this.TileBag,
                              Util.nullable player.Score,
                              Util.nullable (int this.State),
                              Util.nullable ((this.CurrentPlayerPosition + 1) % players.Length))

Questo codice esegue la stored procedure spUpdateGame dal database.Il provider del tipo F# crea un metodo nella classe derivata DataContext- per ogni stored procedure.

CREATE PROCEDURE [dbo].spUpdateGame
@gameId int,
@playerId int,
@moveNumber int,
@playerTiles varchar(50),
@wordPlayed varchar(50),
@boardLayout varchar(MAX),
@tileBag varchar(MAX),
@score int,
@gameState int,
@currentPlayerPosition int
AS
INSERT INTO Plays
VALUES(@gameId, @wordPlayed, @moveNumber, @score, @playerId, GETDATE())

UPDATE PlayerState
SET PlayerState.Score = @score,
    PlayerState.Tiles = @playerTiles
WHERE PlayerState.GameID = @gameid AND PlayerState.PlayerID = @PlayerId

UPDATE Games
SET Games.GameState = @gameState,
    Games.BoardLayout = @boardLayout,
Games.Tilebag = @tileBag,
Games.CurrentPlayerPosition = @currentPlayerPosition
WHERE Games.Id = @gameId
RETURN 0

Il codice F# utilizza una routine di supporto che converte un valore in un valore NULL.La stored procedure con parametri che consentono valori null.Pertanto, è necessario convertire i valori a Nullable<T> prima di passarle come argomenti.La seguente funzione di conversione, nullable, è richiesta e parte di un modulo Util.

// Utility functions.
module Util =

    let nullable value =
        new System.Nullable<_>(value)

In F#, utilizzare un carattere di sottolineatura anziché un parametro generico quando il parametro per dedurre.In questo caso, il parametro generico viene derivato dal tipo del valore passato.

La classe di WordGrid Game utilizza una coda di Windows Azure per comunicare con un ruolo di lavoro, che archivia le parole che vengono riprodotte in un movimento specificato quindi invia un messaggio a un'altra coda con i risultati.In un'applicazione reale, è possibile creare una raccolta locale per cercare parole in corso, anziché richiamare un altro ruolo di lavoro.A scopo esemplificativo, in questo esempio viene illustrato l'utilizzo di base delle code di Windows Azure per la comunicazione tra i ruoli in un servizio cloud di Windows Azure.È inoltre possibile esplorare l'utilizzo di code di bus del servizio anziché le code di Windows Azure per questo scenario.

Il codice seguente definisce e crea le due code.

do CloudStorageAccount.SetConfigurationSettingPublisher(new System.Action<_, _>(fun configName configSetter  ->
                      // Provide the configSetter with the initial value.
                      configSetter.Invoke( RoleEnvironment.GetConfigurationSettingValue( configName ) ) |> ignore
                      RoleEnvironment.Changed.AddHandler( new System.EventHandler<_>(fun sender arg ->
                        arg.Changes
                        |> Seq.toList
                        |> List.filter (fun change -> change :? RoleEnvironmentConfigurationSettingChange)
                        |> List.map (fun change -> change :?> RoleEnvironmentConfigurationSettingChange)
                        |> List.filter (fun change -> change.ConfigurationSettingName = configName && 
                                                      not (configSetter.Invoke( RoleEnvironment.GetConfigurationSettingValue(configName))))
                        |> List.iter (fun change ->
                            // In this case, the change to the storage account credentials in the
                            // service configuration is significant enough that the role must be
                            // recycled to use the current settings. (For example, the 
                            // endpoint may have changed.)
                            RoleEnvironment.RequestRecycle())))))

    let storageAccount = CloudStorageAccount.FromConfigurationSetting("StorageConnectionString")
    let queueClient = storageAccount.CreateCloudQueueClient()
    let lookupQueue = queueClient.GetQueueReference("lookup")
    let resultsQueue = queueClient.GetQueueReference("results")

do
    lookupQueue.CreateIfNotExist () |> ignore
    resultsQueue.CreateIfNotExist () |> ignore

Infine, il codice invia un messaggio e cerca la risposta.

    member this.TryFindResponse(messageHeader) =
        async {
            let! results = resultsQueue.GetMessagesAsync(32)
            let response = results
                           |> Seq.tryPick (fun message ->
                                    let results = message.AsString
                                    let split = results.Split([|'\n'|])
                                    if (split.[0] = messageHeader) then
                                        Async.Start <| resultsQueue.DeleteMessageAsync(message.Id, message.PopReceipt)
                                        let result = split |> Array.toList |> List.tail
                                        Some(result)
                                    else
                                        None
                                    )
            return response
        }

    // In a dictionary, look up all the words that were played in one move.
    // Return a list of tuples, with the word and whether it was found.
    member this.CheckWordsAsync words =
        async {
            let now = System.DateTime.Now
            let messageHeader = now.ToLongTimeString()
                                + " " + now.ToLongDateString()
                                + " " + this.Players.[this.CurrentPlayerPosition].PlayerId.ToString()
                                + " " + this.GameId.ToString()
            let message = new CloudQueueMessage(String.concat "\n" (messageHeader :: words))

            do! lookupQueue.AddMessageAsync(message)
            let rec findResponse messageHeader n =
                async {
                    let! response = this.TryFindResponse(messageHeader)
                    let result =
                        match response with
                        | Some value -> async { return value }
                        | None -> findResponse messageHeader (n + 1)
                    return! result
                }
            let! result = findResponse messageHeader 0
            return result
                    |> List.map (fun (elem : string)-> elem.Split([|' '|]))
                    |> List.map( fun (elem :string []) -> (elem.[0], elem.[1].ToLower() = "true"))
        }

Le esecuzioni precedenti di codice nel contesto di un form HTTP INVIO e un round trip completo.Ovvero un messaggio e la risposta è necessario prima che la risposta HTTP sia fornita al client.Di conseguenza, il thread che esegue questo codice fa parte di un pool di thread ASP.NET.Un pool di thread dispone di un numero limitato di thread.Se migliaia di utenti svolgono contemporaneamente il gioco, molte richieste di cercare parole potrebbero verificarsi in una scarsa quantità di tempo.Se cercare parole è un lungo processo, chiamate asincrone alla messaggistica API necessarie per consentire sospendere il thread e altre richieste da altri giocatori possano essere elaborate.

Per supportare operazioni asincrone, l'api per la coda di servizi di archiviazione in Windows Azure utilizza il modello fine di inizio/.In questo modello legacy per la programmazione asincrona, è necessario chiamare un metodo separato di di inizio e di fine per ogni operazione asincrona della coda.Ad esempio, si aggiunge un messaggio da primo BeginAddMessage chiamante quindi chiamato EndAddMessage per recuperare il risultato quando è pronto.In F#, è possibile eseguire il wrapping di questi metodi tramite il metodo di Async.FromBeginEnd per chiamare un singolo metodo di estensione asincrono.Creare questi wrapper come mostrato nell'estrazione.Astratto presente nel file AzureHelper.fs nel progetto FsAzureHelper.

// Contains types and functions that support the use of Windows Azure,
// including asynchronous methods for queue operations.
module FsAzureHelpers =

    // Extension methods to support asynchronous queue operations.
    type CloudQueue with
        member this.AddMessageAsync(message) =
            Async.FromBeginEnd(message, (fun (message, callback, state) ->
                                this.BeginAddMessage  (message, callback, state)),
                                this.EndAddMessage)
        member this.ClearAsync() =
            Async.FromBeginEnd((fun (callback, state) -> this.BeginClear(callback, state)),
                                this.EndClear)
        member this.CreateAsync() =
            Async.FromBeginEnd((fun (callback, state) -> this.BeginCreate(callback, state)),
                                this.EndCreate)
        ...

Vedere Flussi di lavoro asincroni (F#).

Uno degli svantaggi di utilizzare il servizio di archiviazione Windows Azure gestisce le code, anziché le code di bus del servizio, è necessario trovare il messaggio di risposta corretta esecuzione di ricerche in ogni messaggio a sua volta.Un'intestazione del messaggio viene creata che deve essere presente nel metodo di TryFindResponse.I messaggi vengono recuperati 32 per volta e intestazioni sono reali per trovare il messaggio corretto.Per ulteriori informazioni sulle code in Windows Azure, vedere Come utilizzare il servizio di archiviazione Coda.

Cenni preliminari sul codice c ASP.NET

Il codice c ASP.NET è costituito da modelli, visualizzazioni e i controller.I tipi di modello rappresentano i dati astratti in quanto verrà visualizzata nell'interfaccia utente.Il progetto MvcWebRole1 e il file GameModels.cs, che si trova nella cartella models, contengono i tipi per PlayerModel, BoardCell, BoardRow, TileRacke GameModel.In alcuni casi, questi tipi F# il wrapping dei tipi.Ad esempio, il tipo di PlayerModel esegue il wrapping della classe di Player e la classe di GameModel esegue il wrapping del tipo del gioco F#.In altri casi, il mapping non è diretto.BoardCell e BoardRow prevedono per rappresentare la scheda del gioco, ma F# contiene solo una classe di GameBoard.La classe TileRack del modello c rappresenta le sezioni del giocatore oltre all'area della pagina Web che visualizzano tali sezioni.

Innanzitutto, è possibile esaminare la classe di PlayerModel.

   // Represents a player in a WordGrid game.
    public class PlayerModel
    {
        Player player;

        public int PlayerID { get { return player.PlayerId; } }
        public UserProfile Profile;

        public PlayerModel(UserProfile userProfile)
        {
            Profile = userProfile;
            player = Player.FindOrCreate(userProfile.UserId, userProfile.UserName);
        }

        public int[] GetAllGameIDs()
        {
            return player.GetGameIDs(false);

        }
        public string[] GetAllGameNames()
        {
            return player.GetGameNames(false);
        }

        public int[] GetMyTurnGameIDs()
        {
            return player.GetGameIDs(true);
        }
        public string[] GetMyTurnGameNames()
        {
            return player.GetGameNames(true);
        }
    }

Questa classe è un wrapper relativamente sottile per la classe di F# Lettore multimediale ma contiene anche UserProfile.La classe di UserProfile è una classe ASP.NET che lega il giocatore a un utente con un nome utente e una password del database ASP.NET SimpleMembership.La definizione può essere presente nel file di AccountModel.cs nella cartella dei modelli di progetto MvcWebRole1.

La classe seguente, TileRack, è una rappresentazione abbastanza semplice di sezioni che un giocatore possibile riprodurre la scheda.

// Represents the tile rack in a Word Grid game.
    public class TileRack
    {
        Tile[] tilesInRack = new Tile[Constants.MaxTilesInRack];

        public TileRack(List<Tile> tiles)
        {
            var count = 0;
            foreach (Tile tile in tiles)
            {
                tilesInRack[count++] = tile;
            }
        }

        public Tile[] Tiles
        {
            get { return tilesInRack; }
        }

        public void UpdateRack(List<Tile> tiles)
        {
            var count = 0;
            foreach (Tile tile in tiles)
            {
                tilesInRack[count++] = tile;
            }
        }

    }

Il tipo sottostante Tile F# viene utilizzato per singole sezioni in scaffale e la raccolta che rappresenta le sezioni viene List<T>, non un elenco di F#, grazie alla funzione di conversione fatto riferimento in precedenza.

La classe di BoardCell contiene informazioni sulle immagini che rappresenteranno le sezioni per i diversi tipi di quadrati (spazi della doppia lettera, ad esempio).

La classe di GameModel esegue il wrapping del tipo F# Game e raccogliere i riferimenti alla scheda e allo scaffale la sezione, oltre a informazioni sul giocatore.Il codice seguente viene illustrata la classe di GameModel, con alcune proprietà e metodi omessi.

// Represents a player's WordGrid session, the game that they're currently
    // playing, and all its elements.
    public class GameModel
    {
        Game game;
        BoardRow[] rows;
        TileRack rack;
        Player currentPlayer;
        UserProfile currentUser;
        int userPlayerId;

        // From a Game object and the current user, construct the information that's needed
        // to render a particular play layout for a user's game.
        public GameModel(Game gameIn, UserProfile currentUserIn)
        {
            game = gameIn;
            currentUser = currentUserIn;
            rows = new BoardRow[Constants.BoardSize];
            for (int row = 0; row < Constants.BoardSize; row++)
            {
                rows[row] = new BoardRow(game, row);
            }

            // Identify the Player object and the
            // playerId of the current user
            int playerId = 0;
            bool found = false;
            int index = 0;
            int indexOfCurrentPlayer = 0;
            while (! found) 
            {
                Player player = game.Players[index];
                if (player.UserId == currentUser.UserId)
                {
                    playerId = player.PlayerId;
                    indexOfCurrentPlayer = index;
                    found = true;
                }
                index++;
            }
            if (!found)
                throw new InvalidOperationException();

            currentPlayer = game.Players[indexOfCurrentPlayer];
            userPlayerId = playerId;
            List<Tile> tiles = currentPlayer.GetTilesAsList();
            rack = new TileRack(tiles);
        }

        // Create a game given an array of user profiles who'll
        // play.
        public static GameModel NewGame(UserProfile[] playerProfiles)
        {

            Player[] players = new Player[2];
            int count = 0;
            foreach (UserProfile profile in playerProfiles)
            {      
                players[count++] = Player.FindOrCreate(profile.UserId, profile.UserName);
            }
            Game game = new Game(players);
            game.StartGame();
            return new GameModel(game, playerProfiles[0]);
        }

        // Retrieves a game that's already in progress.
        public static GameModel GetByID(int id, UserProfile currentUser)
        {
            Game game = new Game(id);
            return new GameModel(game, currentUser);
        }

        // Tries to play a move. Processes the move that the client sent.
        public string PlayMove(Move move)
        {
            string userMessage = game.ProcessMove(userPlayerId, move);
            rack.UpdateRack(game.GetPlayerById(userPlayerId).GetTilesAsList());
            return userMessage;
        }

Le visualizzazioni utilizzano le classi di modello precedenti per formare le pagine Web che i giocatori vedere.Le visualizzazioni sono state create dal motore di visualizzazione Razor, la cui proprie sintassi e struttura e utilizza l'estensione di file cshtml.Tramite il motore di visualizzazione Razor, è possibile scrivere una pagina Web HTML e includere il codice c inline in base alle necessità per generare parti delle pagine.In su esempi, il codice c visualizza Razor viene visualizzata la scheda e lo scaffale la sezione utilizzando i dati dalle relative classi del modello c.La parte superiore della visualizzazione contiene il riferimento al modello contenente i dati dalla visualizzazione utilizza.Il motore di visualizzazione Razor interpreta una sintassi che è contrassegnata da "al simbolo @.Tutto il resto viene HTML.Vedere Un'introduzione alla programmazione Web ASP.NET utilizzando la sintassi Razor.

Il codice seguente viene illustrata la visualizzazione per la scheda principale del gioco, nel file Play.cshtml nella sottocartella di visualizzazioni del progetto MvcWebRole1.Parte del layout di pagina viene omesso.

@model MvcWebRole1.Models.GameModel

@using MvcWebRole1.Models
@using WordGridGame

@{
    ViewBag.Title = "Board";
    Layout = "~/Views/Shared/_LayoutBoard.cshtml";
}
<script src="@Url.Content("~/Scripts/WordGridScript.js")" type="text/javascript"></script>

<!-- The game board consists of a series of rows and a series of cells in the rows.
    Each cell is either occupied or unoccupied and, if occupied, contains a tile.
     -->
<style>
...
</style>
@using (@Html.BeginForm("Move", null, FormMethod.Post, new { name = "wordgrid", onsubmit = "return capturePlayDetails(event)" }))
{
    @Html.AntiForgeryToken()
    <h2>WordGrid</h2>
    <p id="userMessage">@Model.UserMessage</p>
<table id="board" class="board">
@foreach (BoardRow row in Model.Rows)
{
    <tr class="row">
    @{
    foreach (BoardCell cell in row.Cells)
    {
            if (cell.IsEmpty)
            {
               <td class="cell"> <div class="boardSpace" ondrop="dropItem(event)" ondragover="allowDrop(event)" id="@cell.CellID" onclick="OnClick(event)">
                   <img id="img@(cell.CellID)" class="@cell.ClassName" alt="@((int)cell.Space)" src="@cell.ImagePath" width="24" /></div>
               </td>
            }
            else
            {           
               <td class="cell"> <div class="boardSpace" ondrop="dropItem(event)" ondragover="allowDrop(event)" id="@cell.CellID" onclick="OnClick(event)">
                  <img id="img@(cell.CellID)" class="@cell.ClassName" alt="@cell.Letter@cell.PointValue" src="@cell.ImagePath" width="24" /></div>
               </td>
            }
    }   
    }
    </tr>
}
</table>


    <input name="row" id="row" type="hidden" value="" />
    <input name="col" id="col" type="hidden" value="" />
    <!-- <input name="word" type="hidden" value="" />-->
    <input name="direction" id="direction" type="hidden" value="" />
    <input name="gameID" id="gameID" type="hidden" value="@Model.GameID" />
    <input name="playerID" id="playerID" type="hidden" value="@Model.UserPlayerID" />
    <input name="tiles" id="tiles" type="hidden" value="" />
    <input name="remainingTiles" id="remainingTiles" type="hidden" value="" />

    <input type="submit" id="submitPlay" value="Submit Play" disabled="disabled" />

    if (Model.IsUsersTurn)
    {
        <input type="button" id="swapTiles" value="Swap Tiles" onclick="swapTiles()" />
    }
}

Nella figura seguente viene mostrata la pagina Web che produce questa visualizzazione.

Esempio WordGrid

ASP.NET compila il codice in una classe c che genera un flusso HTML nella visualizzazione viene richiamato da un controller in risposta alle richieste HTML.Notare quanto segue il codice.

  • Alcuni valori globali importanti sono impostate nella sezione superiore.Ad esempio, ViewBag.Title imposta il titolo della pagina.

  • Il codice fa riferimento a una visualizzazione del layout della riga.

    Layout = "~/Views/Shared/_LayoutBoard.cshtml"
    

    Il file di _LayoutBoard.cshtml contiene una visualizzazione che include un layout master delle pagine del sito.Se più pagine utilizzano lo stesso layout, non è necessario ripetere qualsiasi nella visualizzazione per ogni pagina.

  • Il tag di script contiene un riferimento a un file JavaScript nella cartella scripts dello stesso progetto.Questo file di script contiene la logica lato client JavaScript, incluso il codice che consente di giocatori di trascinamento affianca tra lo scaffale e la scheda, il tratto viene spostato in gioco e determinare se un movimento è valido.

  • La seguente riga di codice, utilizzando @Html.BeginForm, viene avviato il form principale, che consente l'esecuzione di giocatori inviare i movimenti come richieste POST HTTP.

    @using (@Html.BeginForm("Move", null, FormMethod.Post, new { name = "wordgrid", onsubmit = "return capturePlayDetails(event)" }))
    

    Il tipo anonimo nell'ultimo parametro specifica gli attributi per il form, incluso il riferimento alla funzione di capturePlayDetails JavaScript.Questa funzione analizza le informazioni del gioco e impostare i valori degli elementi nascosti ovvero elementi di input il cui tipo è impostato su nascosto) nella pagina che codifica il movimento del giocatore.

  • Il riferimento a @Html.AntiForgeryToken è una misura di sicurezza da utilizzare negli invii del form per impedire determinati tipi di attacchi.

  • La sintassi seguente inserisce i valori nel flusso HTML richiamando il modello (in questo caso, la classe di GameModel ).

    <p id="userMessage">@Model.UserMessage</p>
    

    Funzionamento di questa sintassi perché UserMessage è una proprietà della classe di GameModel.

  • Il codice utilizza la sintassi Razor @foreach per scorrere le righe della scheda.Inoltre, esplorare le celle in ogni riga, il codice c viene visualizzato tra parentesi graffe @ {e}.

  • All'interno di blocchi di codice c, gli elementi HTML vengono visualizzati letteralmente e vengono generati.In HTML, il codice fa riferimento ai valori variabili da c, di nuovo utilizzando @, come illustrato nel codice.

    <img id="img@(cell.CellID)" class="@cell.ClassName" alt="@((int)cell.Space)" src="@cell.ImagePath" width="24" />
    
  • Il giocatore sceglie il pulsante Invia Play per inviare un movimento.Questo elemento chiama la funzione di submitPlay JavaScript, come illustrato nel codice.

    <input type="submit" id="submitPlay" value="Submit Play" disabled="disabled" />
    

    Il pulsante Invia Play è abilitato solo quando l'utente abbia posizionato le sezioni nella scheda che costituiscono un gioco potenzialmente valido.Ad esempio, due sezioni inserite nella scheda che non si connettono o che non rientrano in una singola riga o colonna non verranno convalidati come movimento valido.Le funzioni JavaScript vengono chiamate per verificare la validità di un gioco ogni volta che un giocatore sposta le sezioni.Questa operazione differisce da verificare se un movimento dato fa le parole valide, che viene eseguito sul server.

    I giocatori che non possono costituire una parola o scegliere il pulsante Scambia riquadri, che restituisce le sezioni ai bagagli della sezione, il repository di sezioni inutilizzate e disegna lo stesso numero di sezioni indietro.

Il file BoardController.cs contiene la classe di BoardController, che include metodi per gestire i diversi tipi di richieste che si verificano nella pagina della scheda del gioco.Le richieste GET HTML si verificano ogni volta che un giocatore visualizza un gioco corrente.Richieste POST HTML si verificano quando il giocatore invia un movimento per valutare.

    // This class accepts incoming HTTP requests and
    // determines the appropriate action, which could
    // be a Word Grid move (see Move and Play), swapping tiles (see Swap),
    // or creating a game (see NewGame and CreateGame).
    [ValidateInput(true)]
    public class BoardController : Controller
    {

        int x;

        GameModel gameModel;

        // GET: Board/Play
        // Handles the display of the main game board when a turn starts.
        public ActionResult Play(int gameID, string userMessage)
        {
            var currentUser = GetCurrentUserProfile();
            gameModel = GameModel.GetByID(gameID, currentUser);
            gameModel.UserMessage = userMessage;
            ViewBag.Title = "Word Grid";
            return View(gameModel);
        }

        // POST: Board/Move
        // Handles the main game board's form submission, which occurs
        // when a player submits a move to the server.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Move(FormCollection formValues)
        {
            int gameId = Int32.Parse( formValues["gameId"]);
            int playerId = Int32.Parse( formValues["playerId"]);
            int row = Int32.Parse( formValues["row"]);
            int col = Int32.Parse( formValues["col"]);
            int direction = Int32.Parse(formValues["direction"]);
            string tiles = formValues["tiles"];
            string remainingTiles = formValues["remainingTiles"];
            WordGridGame.Direction directionOfPlay;
            switch (direction)
            {
                case 0: directionOfPlay = WordGridGame.Direction.Across;
                    break;
                case 1: directionOfPlay = WordGridGame.Direction.Down;
                    break;
                case 2: directionOfPlay = WordGridGame.Direction.Single;
                    break;
                default: directionOfPlay = WordGridGame.Direction.Across;
                    break;
            }

            var move = new WordGridGame.Move(gameId, playerId, row, col, directionOfPlay, tiles, remainingTiles);
            var user = GetCurrentUserProfile();
            gameModel = GameModel.GetByID(move.GameID, user);
            string userMessage = gameModel.PlayMove(move);

            return RedirectToAction("Play", new { gameId, userMessage });
        }

        // GET: Board/Swap
        // Processes a tile swap move.
        [HttpGet]
        public ActionResult Swap(string gameId, string playerId, string tilesToSwap)
        {
            var user = GetCurrentUserProfile();
            gameModel = GameModel.GetByID(Int32.Parse(gameId), user);
            if (gameModel.UserPlayerID == Int32.Parse(playerId))
            {
                gameModel.SwapTiles(tilesToSwap);
            }
            string userMessage = "You've swapped tiles.";
            return RedirectToAction("Play", new { gameId, userMessage });
        }

Si notino le seguenti informazioni sul codice precedente:

  • ValidateInputAttribute è impostato su true come misura di sicurezza, per impedire attacchi in cui un utente malintenzionato tenta di inserire il codice HTML in cui l'unico testo è previsto.

  • I metodi di una classe controller rispondono a tipi specifici di URL.Il nome della classe controller costituisce la parte dell'URL, pertanto BoardController gestisce le richieste con Board nell'URL e il metodo Play gestisce gli URL del form Board/Play.Gli argomenti del metodo del controller di Play derivati dai parametri, da gameID e da userMessageURL.I parametri omessi producono valori predefiniti.L'url seguente provocherebbe la chiamata a Play con gameID uguale a 13 e a userMessage uguale a una stringa vuota:

    http://sitename.cloudapp.net/Board/Play?gameID=13
    

    Il motore ASP.NET gestisce l'elaborazione dei parametri URL e determina il metodo corrisponde all'URL passato.

  • Il metodo di azione utilizza FormCollection come parametro.I dati necessari vengono inviate come valori degli elementi del form, che possono essere estratti da FormCollection.È anche possibile utilizzare un oggetto come parametro e avere i valori del form si suppone automaticamente.

Cenni preliminari sul ruolo di lavoro F#

Il ruolo di lavoro di F# nelle parole nel dizionario.In questa parte dell'esempio richiede un archivio dizionario, che l'esempio non include, sotto forma di file di testo.È possibile utilizzare diversi file dizionario online, con il copyright e contratti di licenza varianti.È inoltre possibile utilizzare il correttore di Word se lo stato installato nel server.

Il ruolo di lavoro controlla una coda di richieste per le parole cercare.Ogni messaggio della coda corrisponde a un singolo gioco, sebbene ogni gioco può contenere più parole poiché i giocatori possono creare numerose parole incrociate diverse di un singolo gesto.Una risposta è costituito da un messaggio con parole candidate e un valore true o false che indica se la parola era nel dizionario.

Il seguente codice implementa il ruolo di lavoro.

module DictionaryInfo =
    let dictionaryFilename = "dictionary.txt"

#if USE_MSWORD_DICTIONARY
// Includes functionality for looking up words in the Word
// spelling dictionary.
type WordLookup() =
    inherit obj()
    let app = new Microsoft.Office.Interop.Word.ApplicationClass()
    do app.Visible <- false
    do app.AutoCorrect.ReplaceText <- false
    do app.AutoCorrect.ReplaceTextFromSpellingChecker <- false
    let refTrue = ref (box true)
    let refFalse = ref (box false)

    // Determines whether a list of words is valid according to the
    // Word spelling dictionary.
    member this.CheckWords(words) =
        // Change to lowercase here because proper nouns will be reported as
        // misspelled if they're lowercase.
        let words = List.map (fun (word : string) -> word.ToLower()) words

        let doc1 = app.Documents.Add(Visible = refFalse)
        let text = String.concat(" ") words
        doc1.Words.First.InsertBefore(text)
        let errors = doc1.SpellingErrors
        let numError = errors.Count

        let invalidWords = seq { for error in errors do
                                     yield error.Text
                                      }
                           |> List.ofSeq
        doc1.Close(SaveChanges = refFalse)

        let isValid word = not (List.exists (fun element -> element = word) invalidWords)
        List.map (fun element -> isValid element) words
        |> List.zip (List.map (fun (word: string) -> word.ToUpper()) words)

    // Close the instance of Microsoft Word
    override this.Finalize() =
        app.Quit(refFalse)
        base.Finalize()
#endif

// This worker role looks words in the dictionary. When the user makes a move,
// a message is submitted to a queue "lookup," which contains the words to look up for that
// move. The role looks up the words and submits a message back to the "results" queue, which
// contains the information about whether each word is valid.
type WorkerRole() =
    inherit RoleEntryPoint() 

    let log message kind = Trace.WriteLine(message, kind)
    do CloudStorageAccount.SetConfigurationSettingPublisher(new System.Action<_, _>(fun configName configSetter  ->
                      // Provide the configSetter with the initial value.
                      configSetter.Invoke( RoleEnvironment.GetConfigurationSettingValue( configName ) ) |> ignore
                      RoleEnvironment.Changed.AddHandler( new System.EventHandler<_>(fun sender arg ->
                        arg.Changes
                        |> Seq.toList
                        |> List.filter (fun change -> change :? RoleEnvironmentConfigurationSettingChange)
                        |> List.map (fun change -> change :?> RoleEnvironmentConfigurationSettingChange)
                        |> List.filter (fun change -> change.ConfigurationSettingName = configName && 
                                                      not (configSetter.Invoke( RoleEnvironment.GetConfigurationSettingValue(configName))))
                        |> List.iter (fun change ->
                            // In this case, the change to the storage account credentials in the
                            // service configuration is significant enough that the role must be
                            // recycled to use the most recent settings. (For example, the 
                            // endpoint may have changed.)
                            RoleEnvironment.RequestRecycle())))))
    let storageAccount = CloudStorageAccount.FromConfigurationSetting("StorageConnectionString")
    let blobClient = storageAccount.CreateCloudBlobClient()
    let blobContainer = blobClient.GetContainerReference("WordGrid")
    let queueClient = storageAccount.CreateCloudQueueClient()

#if USE_MSWORD_DICTIONARY
    let wordLookup = new WordLookup()
#endif

    // An in-memory representation of the word list that's used in this game.
    let dictionary =
            let blob = blobContainer.GetBlobReference(DictionaryInfo.dictionaryFilename)
            let blobString = blob.DownloadText()
            blobString.Split([|'\r'; '\n'|], StringSplitOptions.RemoveEmptyEntries)

    // Looks up a single word in the dictionary.
    member this.IsValidWord(word) =
        Seq.exists (fun elem -> elem = word) dictionary

    // Verify the words that were played in a move.
    member this.CheckWords(words) =
#if USE_MSWORD_DICTIONARY
        wordLookup.CheckWords()        
#else        
        List.map (fun word -> this.IsValidWord(word)) words
        |> List.zip words
#endif

    // This is the main message processing loop for the F# worker role.
    override this.Run() =
        log "WorkerRole1 entry point called" "Information"
        let lookupQueue = queueClient.GetQueueReference("lookup")
        let resultQueue = queueClient.GetQueueReference("results")

        lookupQueue.CreateIfNotExist() |> ignore
        resultQueue.CreateIfNotExist() |> ignore

        while(true) do 
            let message = lookupQueue.GetMessage()
            if (message = null) then
                Thread.Sleep(1000)
            else
                lookupQueue.DeleteMessage(message)
                // Messages contain a token (ID) on the first line and
                // then a list of words to look up for a move, one word per line.
                // The return message contains the same token (ID) and
                // the list of words with a Boolean IsValid value for each word.
                let messageString = message.AsString
                let buildMessage id checkResults =
                    let body = List.map (fun (word, isValid) -> String.Format("{0} {1}", word, isValid)) checkResults
                                |> String.concat "\n" 
                    String.Concat(id + "\n", body)
                let newMessage =
                    match (messageString.Split([| '\n' |]) |> Array.toList) with
                    | head :: tail -> new CloudQueueMessage(buildMessage head (this.CheckWords(tail)))
                    | [] -> new CloudQueueMessage("Unknown error in parsing a message in the lookup queue.")
                resultQueue.AddMessage(newMessage)
            log "Working" "Information"

    // Perform any initial configuration on startup.
    override this.OnStart() = 
        // Set the maximum number of concurrent connections. 
        ServicePointManager.DefaultConnectionLimit <- 12
        base.OnStart()

Si notino le seguenti informazioni sul codice precedente:

  • Il modello di base per un ruolo di lavoro include una classe di WorkerRole che eredita da RoleEntryPoint, che contiene un metodo di Run e un metodo di OnStart.

  • Il metodo execute accetta un solo messaggio alla coda, cerca le parole e invia un messaggio di risposta.

  • In questa implementazione, i messaggi vengono elaborati in modo sincrono.In un computer con più core, potrebbe essere necessario elaborare i messaggi in modo asincrono su thread diversi.

  • Il correttore di Word relativamente facile da utilizzare tramite il modello a oggetti di Word e l'assembly di interoperabilità.Per consentire l'utilizzo del dizionario, aggiungere la seguente riga di codice all'inizio del file WorkerRole.fs.

    #define USE_MSWORD_DICTIONARY
    

    Il correttore di Word include numerose parole che non sono valide in genere in questi tipi di riproduzione di parole.Vedere Cenni preliminari sul modello a oggetti di Word.

Passaggi successivi

Provare a estendere e aggiornare il codice.È possibile eseguire i seguenti relativamente brevi progetti di codice con questo esempio.

  • Anziché le code di Windows Azure, utilizzare una coda di bus del servizio.

  • Implementare una funzionalità che è comune ai giochi di parole online ma che non viene implementato, come la capacità di richiamare contemporaneamente le sezioni di nuovo allo scaffale, sezioni di riordinamento dello scaffale, cronologia del gioco, o rinunciano a un gioco.

  • Controllare up ha pubblicato algoritmi del gioco AI di parole incrociate e crea un ruolo di lavoro F# per implementarlo.Alternative di implementare per riprodurre le sezioni (diverso dal trascinamento), consentendo di giocatori possono riprodurre WordGrid su più tipi di dispositivi.

Vedere anche

Attività

Procedura dettagliata: pubblicazione di un'applicazione MVC F#/C# MVC in Windows Azure

Altre risorse

Esempi e procedure dettagliate di Visual F#

Visual F#

ASP.NET MVC 4