Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Sudoku per Windows Phone 7
Adam Miller
Negli ultimi 10 anni il sudoku ha raggiunto un'immensa popolarità e occupa spesso una colonna nella maggior parte dei quotidiani, accanto all'angolo dei cruciverba. Sono stati realizzati quiz televisivi basati sul sudoku. Per chi non lo sapesse, si tratta di un gioco il cui obiettivo consiste nel posizionare all'interno di una griglia composta da 9 caselle orizzontali e 9 verticali i numeri da 1 a 9, in modo tale che un numero compaia una sola volta in ogni riga, in ogni colonna e in ogni griglia. Per la sua natura, questo tipo di gioco è ideale per un dispositivo portatile... e Windows Phone 7 non fa eccezione. In seguito al rilascio di Windows Phone 7, è in arrivo una grande quantità di applicazioni Sudoku, a cui sarà possibile aggiungerne una personalizzata seguendo le istruzioni indicate nel presente articolo.
Introduzione a MVVM
L'applicazione di esempio seguirà a grandi linee il pattern di progettazione Model-View-ViewModel (MVVM). Sebbene non esistano modelli effettivi (poiché l'applicazione non richiede l'archiviazione database), si tratta comunque di un ottimo strumento di apprendimento, in quanto ViewModel rappresenta il cuore del pattern.
È prevista una curva di apprendimento nella conoscenza del pattern MVVM, ma una volta superata, sarà possibile ottenere una netta separazione tra interfaccia utente e logica business. Emergerà inoltre l'efficacia dell'associazione dati in Silverlight, che eviterà all'utente l'immissione di gran parte del codice necessario per aggiornare un'interfaccia grafica (FirstNameTextBox.Text = MyPerson.FirstName sarà solo un lontano ricordo). Per ulteriori informazioni sull'associazione dati in Silverlight, leggere l'articolo MSDN Library "Data Binding" all'indirizzo tinyurl.com/SLdatabind.
Per dimensioni e semplicità di questa applicazione, e ai fini di questo articolo, non verrà utilizzato un framework MVVM di terze parti. L'applicazione finale potrebbe tuttavia risultare più complessa rispetto a quella di esempio ed è pertanto opportuno iniziare con un framework di terze parti, ad esempio MVVM Light Toolkit (mvvmlight.codeplex.com), che fornisce codice gratuito e testato che gli utenti si ritroverebbero comunque a dover scrivere (l'esperienza insegna).
Creazione dell'applicazione
Dopo aver installato gli strumenti di sviluppo dalla pagina xbox.http://xbox.create.msdn.com, creare un nuovo progetto Windows Phone 7 aprendo Visual Studio e facendo clic su File | Nuovo | Progetto e, nella nuova finestra di dialogo, scegliere Visual C# | Silverlight per Windows Phone | Applicazione Windows Phone. Creare due nuove cartelle, Visualizzazioni e ViewModel, seguendo un pattern MVVM comune. A questo punto sarà inoltre possibile avviare il debug, se si desidera controllare l'emulatore fornito nell'ambito dell'SDK.
Il sudoku può essere scomposto in tre tipi concettuali: ogni singolo quadrato (81 in totale nella griglia classica 9x9), la griglia totale che ospita i quadrati e una griglia per l'immissione di numeri da 1 a 9. Per creare le visualizzazioni per questi elementi, fare clic con il pulsante destro del mouse sulla cartella Visualizzazioni e scegliere Aggiungi | Nuovo elemento. Fare clic su Controllo utente Windows Phone e denominare il primo file GameBoardView.xaml. Ripetere il procedimento per SquareView.xaml e InputView.xaml. Aggiungere ora nella cartella ViewModel le classi seguenti: GameBoardViewModel e SquareViewModel. La visualizzazione Input non richiederà ViewModel. È opportuno creare anche una classe di base per ViewModel al fine di evitare la duplicazione del codice. Aggiungere una classe ViewModelBase alla cartella ViewModel. A questo punto la soluzione sarà simile a quella indicata nella Figura 1.
Figura 1 Soluzione sudoku di Windows Phone 7 con visualizzazioni e ViewModel
Classe di base ViewModel
La classe ViewModelBase dovrà implementare l'interfaccia INotifyPropertyChanged individuata in System.ComponentModel. Questa interfaccia consente l'associazione di proprietà pubbliche in ViewModel a controlli nelle visualizzazioni. L'implementazione dell'interfaccia INotifyPropertyChanged è piuttosto semplice, in quanto coinvolge esclusivamente l'evento PropertyChanged. La classe ViewModelBase.cs realizzata sarà simile a quella indicata di seguito (non dimenticare l'istruzione using per System.ComponentModel):
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler
PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(info));
}
}
}
La maggior parte dei framework MVVM di terze parti include una classe di base ViewModel contenente questo codice con testo standard. Tutti i ViewModel erediteranno da ViewModelBase. Le proprietà nel ViewModel a cui verrà associata l'interfaccia utente dovranno chiamare NotifyPropertyChanged nel setter. In questo modo l'interfaccia utente verrà automaticamente aggiornata nel momento in cui viene modificato il valore di una proprietà. Può risultare piuttosto seccante dover implementare tutte le proprietà in questo modo. Un giusto compromesso è rappresentato dal fatto di non dover scrivere il codice per aggiornare l'interfaccia utente.
Implementazione dei singoli quadrati
Implementare la classe SquareViewModel. Aggiungere proprietà pubbliche per Value, Row e Column sotto forma di numeri interi e per IsSelected, IsValid e IsEditable sotto forma di valori booleani. Sebbene l'interfaccia utente possa essere direttamente associata alla proprietà Value, si verificheranno alcuni problemi, poiché per i quadrati non assegnati verrà visualizzato un valore "0". Per risolvere il problema, è possibile implementare un convertitore di associazioni o creare una proprietà "StringValue" di sola lettura che restituirà una stringa vuota se la proprietà Value è 0.
La classe SquareViewModel indicherà inoltre all'interfaccia utente lo stato corrente. I quattro stati per un singolo quadrato in questa applicazione sono Default, Invalid, Selected e UnEditable. Un quadrato viene in genere implementato come enumerazione, ma le enumerazioni nel framework Silverlight non dispongono di alcuni metodi presenti invece in Microsoft .NET Framework. In fase di serializzazione verrà pertanto generata un'eccezione e gli stati vengono implementati come costanti:
public class BoxStates
{
public const int Default = 1;
public const int Invalid = 2;
public const int Selected = 3;
public const int UnEditable = 4;
}
Aprire ora SquareView.xaml. Come si può notare, sono stati applicati alcuni stili a livello del controllo per le dimensioni dei caratteri e il colore. Le risorse di stile predefinite si trovano in genere in un file di risorse distinto, ma in questo caso sono disponibili per impostazione predefinita in Windows Phone 7. Le risorse sono descritte nella pagina di MSDN Library "Theme Resources for Windows Phone" all'indirizzo tinyurl.com/WP7Resources. Alcuni di questi stili verranno utilizzati nella nostra applicazione e pertanto i colori dell'applicazione corrisponderanno al tema selezionato dall'utente. È possibile selezionare il tema nell'emulatore passando alla schermata principale e facendo clic sulla freccia più, quindi su Impostazioni | Tema. A questo punto sarà possibile modificare i colori dello sfondo e in primo piano (Figura 2).
Figura 2 Schermata Windows Phone 7 per l'impostazione del tema
All'interno della griglia di SquareView.xaml posizionare un elemento Border e un elemento TextBlock:
<Grid x:Name="LayoutRoot" MouseLeftButtonDown=
"LayoutRoot_MouseLeftButtonDown">
<Border x:Name="BoxGridBorder"
BorderBrush="{StaticResource PhoneForegroundBrush}"
BorderThickness="{Binding Path=BorderThickness}">
<TextBlock x:Name="MainText"
VerticalAlignment="Center" Margin="0" Padding="0"
TextAlignment="Center" Text=
"{Binding Path=StringValue}">
</TextBlock>
</Border>
</Grid>
Il code-behind di SquareView.xaml.cs è disponibile nel download del codice fornito. Il costruttore necessita di un'istanza di SquareViewModel, che verrà fornita una volta associata la griglia di gioco. Verrà inoltre generato un evento personalizzato nel momento in cui l'utente fa clic all'interno della griglia. L'utilizzo di eventi personalizzati consente a ogni singolo ViewModel di comunicare in modo reciproco. Per le applicazioni di dimensioni superiori questo approccio può tuttavia risultare più complesso. Un'altra opzione consiste nell'implementare una classe Messenger in grado di agevolare la comunicazione. La maggior parte dei framework MVVM fornisce una classe Messenger (a volte nota come Mediator).
Per i puristi dei framework MVVM l'aggiornamento dell'interfaccia utente mediante code-behind potrebbe risultare improprio, ma questi elementi non si prestano facilmente all'utilizzo con BindingConverter. L'elemento BorderThickness di BoxGridBorder si basa su due proprietà e i pennelli Foreground e Background derivano dalle risorse dell'applicazione, non del tutto accessibili in un BindingConverter.
Implementazione della griglia di gioco
È ora possibile implementare la visualizzazione GameBoard e ViewModel. La visualizzazione è semplice (una griglia 9x9). Il code-behind, disponibile nel download del codice, è anch'esso piuttosto semplice: sono sufficienti una proprietà pubblica che esponga ViewModel e pochi metodi privati per gestire i clic sulle caselle e associare l'array di gioco.
ViewModel contiene la parte più consistente del codice. Nello specifico include metodi per convalidare la griglia in seguito all'input dell'utente, risolvere il rompicapo e caricare la griglia dall'archiviazione. La griglia viene serializzata in XML in fase di salvataggio e per salvare il file viene utilizzato IsolatedStorage. Per l'implementazione completa osservare il download del codice sorgente: il codice di archiviazione è di particolare interesse ed è illustrato nella Figura 3 (è necessario un riferimento a System.Xml.Serialization).
Figura 3 Codice di archiviazione della griglia
public void SaveToDisk()
{
using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
{
if (store.FileExists(FileName))
{
store.DeleteFile(FileName);
}
using (IsolatedStorageFileStream stream = store.CreateFile(FileName))
{
using (StreamWriter writer = new StreamWriter(stream))
{
List<SquareViewModel> s = new List<SquareViewModel>();
foreach (SquareViewModel item in GameArray)
s.Add(item);
XmlSerializer serializer = new XmlSerializer(s.GetType());
serializer.Serialize(writer, s);
}
}
}
}
public static GameBoardViewModel LoadFromDisk()
{
GameBoardViewModel result = null;
using (IsolatedStorageFile store = IsolatedStorageFile.
GetUserStoreForApplication())
{
if (store.FileExists(FileName))
{
using (IsolatedStorageFileStream stream =
store.OpenFile(FileName, FileMode.Open))
{
using (StreamReader reader = new StreamReader(stream))
{
List<SquareViewModel> s = new List<SquareViewModel>();
XmlSerializer serializer = new XmlSerializer(s.GetType());
s = (List<SquareViewModel>)serializer.Deserialize(
new StringReader(reader.ReadToEnd()));
result = new GameBoardViewModel();
result.GameArray = LoadFromSquareList(s);
}
}
}
}
return result;
}
Implementazione della griglia di input
Anche la visualizzazione Input è semplice, in quanto è composta da alcuni pulsanti nidificati in StackPanel. Il code-behind, illustrato nella Figura 4, espone un evento personalizzato per inviare il valore del pulsante premuto all'applicazione, nonché due metodi che consentiranno un'esperienza di gioco in modalità orizzontale o verticale.
Figura 4 Code-behind per la visualizzazione Input
public event EventHandler SendInput;
private void UserInput_Click(object sender, RoutedEventArgs e)
{
int inputValue = int.Parse(((Button)sender).Tag.ToString());
if (SendInput != null)
SendInput(inputValue, null);
}
public void RotateVertical()
{
TopRow.Orientation = Orientation.Vertical;
BottomRow.Orientation = Orientation.Vertical;
OuterPanel.Orientation = Orientation.Horizontal;
}
public void RotateHorizontal()
{
TopRow.Orientation = Orientation.Horizontal;
BottomRow.Orientation = Orientation.Horizontal;
OuterPanel.Orientation = Orientation.Vertical;
}
Unione di visualizzazioni in MainPage.xaml
L'applicazione viene infine completata con l'implementazione di MainPage.xaml. Le visualizzazioni Input e GameBoard vengono posizionate in una griglia. Questa applicazione richiederà tutto lo spazio disponibile sullo schermo e sarà pertanto necessario rimuovere l'elemento TextBlock di PageTitle, inserito automaticamente in fase di creazione del progetto. L'elemento TextBlock di ApplicationTitle sarà visibile esclusivamente in modalità verticale. Verrà utilizzata anche la barra delle applicazioni di Windows Phone 7 per garantire un'integrazione maggiore dell'applicazione con il telefono e fornire un'interfaccia che consentirà all'utente di risolvere, azzerare e iniziare un nuovo rompicapo:
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
<shell:ApplicationBarIconButton x:Name="NewGame"
IconUri="/Images/appbar.favs.rest.png" Text="New Game"
Click="NewGame_Click"></shell:ApplicationBarIconButton>
<shell:ApplicationBarIconButton x:Name="Solve"
IconUri="/Images/appbar.share.rest.png" Text="Solve"
Click="Solve_Click"></shell:ApplicationBarIconButton>
<shell:ApplicationBarIconButton x:Name="Clear"
IconUri="/Images/appbar.refresh.rest.png" Text="Clear"
Click="Clear_Click"></shell:ApplicationBarIconButton>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
Le immagini vengono ricavate da un gruppo di icone fornito da Microsoft per Windows Phone 7. Le icone vengono installate con gli strumenti al percorso C:\Programmi (x86)\Microsoft SDKs\Windows Phone\v7.0\Icons. Una volta importate le immagini nel progetto, selezionare le relative proprietà e modificare Operazione di generazione da "Risorsa" a "Contenuto" e Copia nella directory di output da "Non copiare" a "Copia se più recente".
L'ultimo tassello del puzzle consiste nell'implementare il code-behind MainPage. Nel costruttore la proprietà SupportedOrientations è impostata per consentire la rotazione dell'applicazione nel momento in cui l'utente ruota il telefono. Viene inoltre gestito l'evento SendInput di InputView e il valore di input viene inoltrato a GameBoard:
public MainPage()
{
InitializeComponent();
SupportedOrientations = SupportedPageOrientation.Portrait |
SupportedPageOrientation.Landscape;
InputControl.SendInput += new
EventHandler(InputControl_SendInput);
}
void InputControl_SendInput(object sender, EventArgs e)
{
MainBoard.GameBoard.SendInput((int)sender);
}
È inoltre necessario implementare i metodi Navigation per gestire il caricamento e il salvataggio della griglia:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
GameBoardViewModel board =
GameBoardViewModel.LoadFromDisk();
if (board == null)
board = GameBoardViewModel.LoadNewPuzzle();
MainBoard.GameBoard = board;
base.OnNavigatedTo(e);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
MainBoard.GameBoard.SaveToDisk();
base.OnNavigatedFrom(e);
}
Quando il telefono viene ruotato, l'applicazione riceverà una notifica. A questo punto InputView si sposta dalla parte inferiore della griglia al lato destro e viene ruotato (vedere la Figura 5).
Figura 5 Codice per gestire la rotazione del telefono
protected override void OnOrientationChanged(OrientationChangedEventArgs e)
{
switch (e.Orientation)
{
case PageOrientation.Landscape:
case PageOrientation.LandscapeLeft:
case PageOrientation.LandscapeRight:
TitlePanel.Visibility = Visibility.Collapsed;
Grid.SetColumn(InputControl, 1);
Grid.SetRow(InputControl, 0);
InputControl.RotateVertical();
break;
case PageOrientation.Portrait:
case PageOrientation.PortraitUp:
case PageOrientation.PortraitDown:
TitlePanel.Visibility = Visibility.Visible;
Grid.SetColumn(InputControl, 0);
Grid.SetRow(InputControl, 1);
InputControl.RotateHorizontal();
break;
default:
break;
}
base.OnOrientationChanged(e);
}
In questo punto vengono gestiti anche i clic delle voci di menu:
private void NewGame_Click(object sender, EventArgs e)
{
MainBoard.GameBoard = GameBoardViewModel.LoadNewPuzzle();
}
private void Solve_Click(object sender, EventArgs e)
{
MainBoard.GameBoard.Solve();
}
private void Clear_Click(object sender, EventArgs e)
{
MainBoard.GameBoard.Clear();
}
A questo punto il gioco è completato e pronto per l'uso (vedere la Figura 6 e la Figura 7).
Figura 6 Sudoku in modalità verticale
Figura 7 Gioco risolto in modalità orizzontale
Il "gioco" è fatto: un rompicapo divertente per ingannare il tempo. In questo articolo sono state fornite informazioni per creare applicazioni Windows Phone 7 basate su Silverlight e utilizzare la serializzazione e l'archiviazione utente per salvare un'applicazione, oltre ad alcune informazioni per consentire il supporto di più orientamenti per l'applicazione. Gli utenti dovrebbero ora aver acquisito una certa familiarità con il pattern MVVM e i metodi con cui utilizzare l'associazione dati.
Adam Miller è un programmatore di Nebraska Global a Lincoln, Nebraska. È possibile seguire il suo blog all'indirizzo blog.milrr.com.
Un ringraziamento ai seguenti esperti tecnici per la revisione dell'articolo: Larry Lieberman e Nick Sherrill