-
Notifications
You must be signed in to change notification settings - Fork 5
Instructions 06 MAUI2
Sebastian Szvetecz edited this page Sep 28, 2023
·
4 revisions
Implement the View-Models for the game
If you are not interested in developing the view-model library on your own, because you want to focus on something else or you simply have not enough time, you can use our NuGet package
CNInnovation.Codebreaker.ViewModels
. Bear in mind, that you still need to register the view-models in the DI container.
- Open the View-Model library CodeBreaker.ViewModels
- Add a reference to the NuGet package CommunityToolkit.Mvvm and Microsoft.Extensions.Options
- Add the IDialogService - this is a contract to open a dialog
- Add the InfoMessageViewModel and the InfoBarMessageService - this is an alternative API to show a dialog
- Add the SelectedFieldViewModel - this is a simple view-model which will be used for a selection.
- Add the GamePageViewModel
- Add the GameViewModel - this is a view-model just to show information
- Add the MoveViewModel - this is a view-model to make a move
- Create a custom implementation of the IDialogService
public interface IDialogService
{
Task ShowMessageAsync(string message);
}
public class InfoBarMessageService
{
public ObservableCollection<InfoMessageViewModel> Messages { get; } = new();
public void ShowMessage(InfoMessageViewModel message)
{
message.ContainingCollection = Messages;
Messages.Add(message);
}
public void ShowInformation(string content) => ShowMessage(InfoMessageViewModel.Information(content));
public void ShowWarning(string content) => ShowMessage(InfoMessageViewModel.Warning(content));
public void ShowError(string content) => ShowMessage(InfoMessageViewModel.Error(content));
public void ShowSuccess(string content) => ShowMessage(InfoMessageViewModel.Success(content));
public void Clear() =>
Messages.Clear();
}
public enum InfoMessageSeverity
{
Info,
Success,
Warning,
Error
}
public partial class InfoMessageViewModel : ObservableObject
{
public static InfoMessageViewModel Error(string content)
{
InfoMessageViewModel message = new()
{
Title = "Error",
Message = content,
Severity = InfoMessageSeverity.Error,
ActionTitle = "OK"
};
message.ActionCommand = new RelayCommand(() => message.Close());
return message;
}
public static InfoMessageViewModel Warning(string content)
{
InfoMessageViewModel message = new()
{
Title = "Warning",
Message = content,
Severity = InfoMessageSeverity.Warning,
ActionTitle = "OK"
};
message.ActionCommand = new RelayCommand(() => message.Close());
return message;
}
public static InfoMessageViewModel Information(string content)
{
InfoMessageViewModel message = new()
{
Title = "Information",
Message = content,
Severity = InfoMessageSeverity.Info,
ActionTitle = "OK"
};
message.ActionCommand = new RelayCommand(() => message.Close());
return message;
}
public static InfoMessageViewModel Success(string content)
{
InfoMessageViewModel message = new()
{
Title = "Success",
Message = content,
Severity = InfoMessageSeverity.Success,
ActionTitle = "OK"
};
message.ActionCommand = new RelayCommand(() => message.Close());
return message;
}
internal ICollection<InfoMessageViewModel>? ContainingCollection { get; set; }
[ObservableProperty]
private InfoMessageSeverity _severity = InfoMessageSeverity.Info;
[ObservableProperty]
private string _message = string.Empty;
[ObservableProperty]
private string _title = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasAction))]
private ICommand? _actionCommand;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasAction))]
private string? _actionTitle = "OK";
public bool HasAction =>
ActionCommand is not null && ActionTitle is not null;
public void Close() =>
ContainingCollection?.Remove(this);
}
public partial class SelectedFieldViewModel : ObservableObject
{
[ObservableProperty]
private string? _value;
public bool IsSet =>
Value is not null && Value != string.Empty;
public void Reset() =>
Value = null;
}
public enum GameMode
{
NotRunning,
Started,
MoveSet,
Lost,
Won
}
public enum GameMoveValue
{
Started,
Completed
}
public class GamePageViewModelOptions
{
public bool EnableDialogs { get; set; } = false;
}
public partial class GamePageViewModel : ObservableObject
{
private readonly IGameClient _client;
private int _moveNumber = 0;
private GameDto? _game;
private readonly bool _enableDialogs = false;
private readonly IDialogService _dialogService;
public GamePageViewModel(
IGameClient client,
IOptions<GamePageViewModelOptions> options,
IDialogService dialogService)
{
_client = client;
_dialogService = dialogService;
_enableDialogs = options.Value.EnableDialogs;
PropertyChanged += (sender, e) =>
{
if (e.PropertyName == nameof(GameStatus))
WeakReferenceMessenger.Default.Send(new GameStateChangedMessage(GameStatus));
};
}
public InfoBarMessageService InfoBarMessageService { get; } = new();
public GameDto? Game
{
get => _game;
set
{
OnPropertyChanging(nameof(Game));
OnPropertyChanging(nameof(Fields));
_game = value;
Fields.Clear();
for (int i = 0; i < value?.Holes; i++)
{
SelectedFieldViewModel field = new();
field.PropertyChanged += (sender, e) => SetMoveCommand.NotifyCanExecuteChanged();
Fields.Add(field);
}
OnPropertyChanged(nameof(Game));
OnPropertyChanged(nameof(Fields));
}
}
[ObservableProperty]
private string _name = string.Empty;
[NotifyPropertyChangedFor(nameof(IsNameEnterable))]
[ObservableProperty]
private bool _isNamePredefined = false;
public ObservableCollection<SelectedFieldViewModel> Fields { get; } = new();
public ObservableCollection<SelectionAndKeyPegs> GameMoves { get; } = new();
[ObservableProperty]
private GameMode _gameStatus = GameMode.NotRunning;
[NotifyPropertyChangedFor(nameof(IsNameEnterable))]
[ObservableProperty]
private bool _inProgress = false;
[ObservableProperty]
private bool _isCancelling = false;
public bool IsNameEnterable => !InProgress && !IsNamePredefined;
[RelayCommand(AllowConcurrentExecutions = false, FlowExceptionsToTaskScheduler = true)]
private async Task StartGameAsync()
{
try
{
InitializeValues();
InProgress = true;
CreateGameResponse response = await _client.StartGameAsync(Name);
GameStatus = GameMode.Started;
Game = response.Game;
_moveNumber++;
}
catch (Exception ex)
{
InfoMessageViewModel message = InfoMessageViewModel.Error(ex.Message);
message.ActionCommand = new RelayCommand(() =>
{
GameStatus = GameMode.NotRunning;
message.Close();
});
InfoBarMessageService.ShowMessage(message);
if (_enableDialogs)
await _dialogService.ShowMessageAsync(ex.Message);
}
finally
{
InProgress = false;
}
}
[RelayCommand(CanExecute = nameof(CanSetMove), AllowConcurrentExecutions = false, FlowExceptionsToTaskScheduler = true)]
private async Task SetMoveAsync()
{
try
{
InProgress = true;
WeakReferenceMessenger.Default.Send(new GameMoveMessage(GameMoveValue.Started));
if (Game is null)
throw new InvalidOperationException("No game running");
if (Fields.Count != Game.Holes || Fields.Any(x => !x.IsSet))
throw new InvalidOperationException("All colors need to be selected before invoking this method");
string[] selection = Fields.Select(x => x.Value!).ToArray();
CreateMoveResponse response = await _client.SetMoveAsync(Game.GameId, selection);
SelectionAndKeyPegs selectionAndKeyPegs = new(selection, response.KeyPegs, _moveNumber++);
GameMoves.Add(selectionAndKeyPegs);
GameStatus = GameMode.MoveSet;
WeakReferenceMessenger.Default.Send(new GameMoveMessage(GameMoveValue.Completed, selectionAndKeyPegs));
if (response.Won)
{
GameStatus = GameMode.Won;
InfoBarMessageService.ShowInformation("Congratulations - you won!");
if (_enableDialogs)
await _dialogService.ShowMessageAsync("Congratulations - you won!");
}
else if (response.Ended)
{
GameStatus = GameMode.Lost;
InfoBarMessageService.ShowInformation("Sorry, you didn't find the matching colors!");
if (_enableDialogs)
await _dialogService.ShowMessageAsync("Sorry, you didn't find the matching colors!");
}
}
catch (Exception ex)
{
InfoBarMessageService.ShowError(ex.Message);
if (_enableDialogs)
await _dialogService.ShowMessageAsync(ex.Message);
}
finally
{
ClearSelectedColor();
InProgress = false;
}
}
private bool CanSetMove =>
Fields.All(s => s is not null && s.IsSet);
private void ClearSelectedColor()
{
for (int i = 0; i < Fields.Count; i++)
Fields[i].Reset();
SetMoveCommand.NotifyCanExecuteChanged();
}
private void InitializeValues()
{
ClearSelectedColor();
GameMoves.Clear();
GameStatus = GameMode.NotRunning;
InfoBarMessageService.Clear();
_moveNumber = 0;
}
}
public record SelectionAndKeyPegs(string[] GuessPegs, KeyPegsDto KeyPegs, int MoveNumber);
public record class GameStateChangedMessage(GameMode GameMode);
public record class GameMoveMessage(GameMoveValue GameMoveValue, SelectionAndKeyPegs? SelectionAndKeyPegs = null);
public class GameViewModel
{
private readonly GameDto _game;
public GameViewModel(GameDto game)
{
_game = game;
}
public Guid GameId => _game.GameId;
public string Name => _game.Username;
public IReadOnlyList<string> Code => _game.Code;
public IReadOnlyList<string> ColorList => _game.Colors;
public int Holes => _game.Holes;
public int MaxMoves => _game.MaxMoves;
public DateTime StartTime => _game.Start;
public ObservableCollection<MoveViewModel> Moves { get; init; } = new();
}
public class MoveViewModel
{
private readonly MoveDto _move;
public MoveViewModel(MoveDto move) =>
_move = move;
public int MoveNumber => _move.MoveNumber;
public IReadOnlyList<string> GuessPegs => _move.GuessPegs;
public KeyPegsDto? KeyPegs => _move.KeyPegs;
}
public class MauiDialogService : IDialogService
{
public Task ShowMessageAsync(string message)
{
WeakReferenceMessenger.Default.Send(new InfoMessage(message));
return Task.CompletedTask;
}
}
public record InfoMessage(string Text);
Configure the application builder to setup the view-models
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
#if DEBUG
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Development");
#else
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Production");
#endif
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
builder.Configuration.AddJsonStream(FileSystem.OpenAppPackageFileAsync("appsettings.json").Result);
builder.Configuration.AddJsonStream(FileSystem.OpenAppPackageFileAsync($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")}.json").Result);
builder.Services.Configure<GamePageViewModelOptions>(options => options.EnableDialogs = true);
builder.Services.AddScoped<IDialogService, MauiDialogService>();
builder.Services.AddScoped<GamePageViewModel>();
builder.Services.AddHttpClient<IGameClient, GameClient>(client =>
{
client.BaseAddress = new(builder.Configuration.GetRequired("ApiBase"));
});
builder.Services.AddTransient<GamePage>();
return builder.Build();
}
}