This is a simple application that will eventually manage certain files for video games. For now, I'm only posting code that handles fetching, adding, and viewing games.
I'm trying to keep things incredibly simple and pure while learning WPF and MVVM, so my goal is to avoid frameworks. Please note that I'm not too concerned with the UI at this point, so I'm only interested in XAML/UI suggestions if they serve to improve my implementation of MVVM or if I'm missing easy/common shortcuts. Also, I'll omit the service, repository, and ViewModel classes unless they are requested.
Model
[SettingsSerializeAs(SettingsSerializeAs.Xml)]
public class Game : IEntity
{
public string Name { get; set; }
public string ExecutablePath { get; set; } // e.g. Minecraft.exe
public Game() { } // Required to be serialized as a setting.
public Game(string name, string executablePath)
{
Name = name;
ExecutablePath = executablePath;
}
}
MainView and MainViewModel - I won't share the code for these. MainView
simply has a GamesView and a button to open the ManageGamesView
.
GamesView - A simple ComboBox (appears on the MainView above). Used to select the current game.
<UserControl x:Class="ENBOrganizer.UI.Views.GamesView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d"
xmlns:ViewModels="clr-namespace:ENBOrganizer.UI.ViewModels" xmlns:UI="clr-namespace:ENBOrganizer.UI">
<UserControl.Resources>
<UI:PathToIconConverter x:Key="PathToIconConverter" />
</UserControl.Resources>
<UserControl.DataContext>
<ViewModels:GamesViewModel />
</UserControl.DataContext>
<Grid>
<ComboBox ItemsSource="{Binding Games}" SelectedItem="{Binding SelectedGame}"
VerticalAlignment="Top" HorizontalAlignment="Left" MaxHeight="25">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ExecutablePath, Converter={StaticResource PathToIconConverter}, Mode=OneWay}" />
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" Padding="5,0,0,0" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</UserControl>
GamesViewModel
public class GamesViewModel : ViewModelBase
{
private readonly GameService _gameService;
private ObservableCollection<Game> _games;
public ObservableCollection<Game> Games
{
get { return _games; }
set { _games = value; RaisePropertyChanged("Games"); }
}
public Game SelectedGame
{
get { return Settings.Default.SelectedGame; }
set
{
if ((value != null) && (Settings.Default.SelectedGame != value))
{
Settings.Default.SelectedGame = value;
Settings.Default.Save();
RaisePropertyChanged("SelectedGame");
}
}
}
public GamesViewModel()
{
_gameService = ServiceSingletons.GameService;
_gameService.GamesChanged += OnGamesChanged;
Games = new ObservableCollection<Game>(_gameService.GetAll());
}
private void OnGamesChanged(object sender, EventArgs e)
{
Games = new ObservableCollection<Game>(_gameService.GetAll());
}
}
ManageGamesView
- A screen used to add and remove games
<Window x:Class="ENBOrganizer.UI.Views.ManageGamesView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModels="clr-namespace:ENBOrganizer.UI.ViewModels" xmlns:UI="clr-namespace:ENBOrganizer.UI"
Title="Manage Games" MinWidth="250" SizeToContent="WidthAndHeight" >
<Window.Resources>
<UI:PathToIconConverter x:Key="PathToIconConverter" />
</Window.Resources>
<Window.DataContext>
<ViewModels:ManageGamesViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Grid.Row ="0">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Label Content="Name:" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" />
<TextBox Text="{Binding Name}" Grid.Row="0" Grid.Column="1" MinWidth="100" Margin="5" />
<Label Content="Path:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" />
<TextBox Text="{Binding ExePath}" Grid.Row="1" Grid.Column="1" MaxWidth="300" TextWrapping="Wrap" Margin="5" />
<Button Content="Browse..." Command="{Binding BrowseCommand}" Grid.Row="1" Grid.Column="2" VerticalAlignment="Center" Margin="0,0,5,0" />
<StackPanel Grid.Row="2" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right" >
<Button Content="Remove" Command="{Binding RemoveGameCommand}" HorizontalAlignment="Right" Margin="0,0,5,0" />
<Button Content="Add" Command="{Binding AddGameCommand}" HorizontalAlignment="Right" />
</StackPanel>
</Grid>
<StackPanel Grid.Row="1" Orientation="Vertical">
<TextBlock>Games</TextBlock>
<ListView Name="GamesListView" ItemsSource="{Binding Games}" SelectedItem="{Binding SelectedGame}" >
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ExecutablePath, Converter={StaticResource PathToIconConverter}, Mode=OneWay}" />
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" Padding="5,0,0,0" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Grid>
</Window>
It ends up looking like this:
ManageGamesViewModel
public class ManageGamesViewModel : ViewModelBase, IDataErrorInfo
{
private readonly GameService _gameService;
private ObservableCollection<Game> _games;
private Game _selectedGame;
private string _name, _exePath;
public ICommand RemoveGameCommand { get; set; }
public ICommand AddGameCommand { get; set; }
public ICommand BrowseCommand { get; set; }
public ObservableCollection<Game> Games
{
get { return _games; }
set { _games = value; RaisePropertyChanged("Games"); }
}
public Game SelectedGame
{
get { return _selectedGame; }
set
{
_selectedGame = value;
if (value == null)
return;
Name = value.Name;
ExePath = value.ExecutablePath;
}
}
public string Name
{
get { return _name; }
set { _name = value; RaisePropertyChanged("Name"); }
}
public string ExePath
{
get { return _exePath; }
set { exePath = value; RaisePropertyChanged("ExePath"); }
}
public ManageGamesViewModel()
{
_gameService = ServiceSingletons.GameService;
_gameService.GamesChanged += OnGamesChanged;
Games = new ObservableCollection<Game>(_gameService.GetAll());
RemoveGameCommand = new ActionCommand(RemoveGame, CanRemove);
AddGameCommand = new ActionCommand(AddGame, CanAdd);
BrowseCommand = new ActionCommand(BrowseForGameFile, () => true);
}
private void AddGame()
{
Game game = new Game(Name, ExePath);
_gameService.Add(game);
}
private bool CanAdd()
{
Game game = new Game(Name, ExePath);
return _gameService.ValidateGame(game);
}
private void BrowseForGameFile()
{
OpenFileDialog openFileDialog = new OpenFileDialog
{
Filter = "EXE Files (*.exe)|*.exe",
Title = "Select the game's .exe file"
};
if (openFileDialog.ShowDialog() == true)
{
Name = Path.GetFileNameWithoutExtension(openFileDialog.FileName);
ExePath = openFileDialog.FileName;
}
}
private void RemoveGame()
{
_gameService.Delete(SelectedGame);
}
private bool CanRemove()
{
return SelectedGame != null;
}
private void OnGamesChanged(object sender, EventArgs e)
{
Games = new ObservableCollection<Game>(_gameService.GetAll());
}
}
Please note that the SelectedGame
property here is simply what is selected in the ListView. SelectedGame
in GamesViewModel
is an application setting.
Key Concerns
My concerns are pretty much all related to one another. I could be worrying about nothing, but I can't fight this feeling that I'm missing a pattern or solution that would address all of these issues at once:
- I'm using an event,
GamesChanged
, in the GameService class to alert my ViewModel to CRUD operations. This feels wrong and slightly WinForms-ish. I thought of implementingINotifyPropertyChanged
in my service class, but I'm not quite sure that would work since I would have to invent a new property to watch. I also considered not listening to the service at all and manually updating the UI, i.e. adding/removing from theObservableCollection
after a service call. However, issue 2 explains my dilemma there. - Because of issue 1, I needed the same instance of
GameService
in bothGamesViewModel
andManageGamesViewModel
so that they could each react to changes to the data simultaneously, e.g. adding a new game in the Manage Games screen refreshes the ComboBox on the main screen. Assuming I've taken the right approach in using an event from the service, is my use of a singleton property acceptable? In WinForms, I would have passed an instance ofGameService
in the constructor of the form, but I don't believe that's possible for my ViewModel because it is instantiated in XAML without arguments. Since I'm trying to stay very simple, I'd like to avoid an IoC container right now. - This also hinges on issue 1, but I'm skeptical of how I refresh the
ObservableCollection<Game>
property in theGamesViewModel
andManageGamesViewModel
. I do like how simple it is, but it seems I don't even needObservableCollection
here. That could just as well be a plain ol'List<Game>
.
I think that does it for now. Feel free to offer any suggestions. Although I'm trying to avoid frameworks, I'm open to using them if they simplify my design and/or easily resolve my issues.