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