Nicht, dass das Model View View Model (MVVM) der Weisheit letzter Schluß wäre, um wartbare Anwendungen in WPF zu schreiben. Es ist lediglich ein Muster, dass sich in letzter Zeit eingebürgert hat und sich ganz gut anhört: Es gibt ein getrenntes Modell, dass es bei MVC auch stets gibt. Das hat den Vorteil, dass man die Intelligenz der Anwendung testgetrieben entwickeln kann. Aber auch Feinde der testgetriebenen Anwendung sehen in solch einer Trennung Vorteile, bleibt doch der Modellcode rein und lesbar und die Anwendung kann nach einzelnen Aspekten entwickelt werden. Schließlich kann reiner Modellcode den Lauf der Zeiten besser überdauern, da man ihn direkter und einfacher ändern kann, statt irrelevante oder oberflächliche Dinge stets mit in die Betrachtung ziehen zu müssen. Das View-Model ist ein weiteres Modell, allerdings dient es nur der View und beschreibt, nach welchem Modell diese funktioniert. Es gehört also nur zu dieser Sicht auf die Daten.
Um das MVVM-Muster auszuprobieren, programmiere ich mir wieder einen lange gehegten Wunsch. Ich habe 40.000 Go-Spiele als einzelne Datei und als ebenso viele Dateien vorliegen und würde mir gerne eine komfortable Suche gönnen, statt in einer 57 MByte großen Datei mit einem Texteditor nach Spielen zu fischen.
Das Modell ist extrem simpel und für dieses Modell lohnt sich der Aufwand natürlich nicht. Sei es drum:
namespace mvvmfilter
{
class GoGame
{
public string Player1 { get; set; }
public string Player2 { get; set; }
public string Date { get; set; }
public string Tournament { get; set; }
public string Result { get; set; }
public string Content { get; set; }
}
}
Interessanter ist das View-Modell. Es ist der umfangreichste Code in der Projektmappe und das zeigt schon, dass meine Anwendung hauptsächlich aus einer Oberfläche besteht. Das ist auch genau meine Absicht: eine komfortablere Suche. Der Code spiegelt also genau meine Absicht wieder, prima!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using mvvmfilter;
using System.Collections.ObjectModel;
using Microsoft.Practices.Composite.Wpf.Commands;
using System.Windows.Forms;
using System.IO;
namespace mvvmfilter
{
class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<GoGame> GoGameList { get; set; }
public ObservableCollection<GoGame> FilteredList
{
get
{
return GetFilteredList(GoGameList);
}
}
public ViewModel()
{
GoGameList = new ObservableCollection<GoGame>();
SaveEquipmentCommand = new DelegateCommand<object>(SaveEquipment);
foreach (fscharf.GoGame game in fscharf.Games)
GoGameList.Add(new GoGame() { Player1 = game.PlayerWhite, Player2 = game.PlayerBlack, Date = game.Date, Tournament = game.Tournament , Result = game.Result, Content = game.Content});
}
public DelegateCommand<object> SaveEquipmentCommand { get; set; }
private void SaveEquipment(object pObject)
{
Stream myStream;
SaveFileDialog saveFileDialog1 = new SaveFileDialog();
saveFileDialog1.Filter = "sgf files (*.sgf)|*.sgf|All files (*.*)|*.*";
saveFileDialog1.RestoreDirectory = true;
if (saveFileDialog1.ShowDialog() == DialogResult.OK)
{
if ((myStream = saveFileDialog1.OpenFile()) != null)
{
string content = ((GoGame)pObject).Content;
System.IO.StreamWriter file = new System.IO.StreamWriter(myStream);
file.WriteLine(content);
file.Close();
}
}
}
private ObservableCollection<GoGame> GetFilteredList(ObservableCollection<GoGame> pOrginalGoGameList)
{
ObservableCollection<GoGame> filteredGoGamelist = new ObservableCollection<GoGame>();
var x = from p in pOrginalGoGameList
where
FilterPlayer1Method(p, this.FilterPlayer1) &&
FilterPlayer2Method(p, this.FilterPlayer2) &&
FilterDateMethod(p, this.FilterDate) &&
FilterTournamentMethod(p, this.FilterTournament)
select p;
foreach (var u in x)
filteredGoGamelist.Add(u);
return filteredGoGamelist;
}
public static bool FilterPlayer1Method(GoGame goGame, string compare)
{
if (goGame == null) return false;
if (compare == null) return false;
if (compare.Equals(string.Empty)) return true;
return (goGame.Player1.ToUpper().StartsWith(compare.ToUpper()));
}
public static bool FilterPlayer2Method(GoGame goGame, string compare)
{
if (goGame == null) return false;
if (compare == null) return false;
if (compare.Equals(string.Empty)) return true;
return (goGame.Player2.ToUpper().StartsWith(compare.ToUpper()));
}
public static bool FilterDateMethod(GoGame goGame, string compare)
{
if (goGame == null) return false;
if (compare.Equals(string.Empty)) return true;
return (goGame.Date.ToUpper().StartsWith(compare.ToUpper()));
}
public static bool FilterTournamentMethod(GoGame goGame, string compare)
{
if (goGame == null) return false;
if (compare.Equals(string.Empty)) return true;
return (goGame.Tournament.ToUpper().StartsWith(compare.ToUpper()));
}
private string _FilterPlayer2 = string.Empty;
public string FilterPlayer2
{
get
{
return _FilterPlayer2;
}
set
{
_FilterPlayer2 = value;
NotifyPropertyChanged("FilterPlayer2");
NotifyPropertyChanged("FilteredList");
}
}
private string _FilterPlayer1 = string.Empty;
public string FilterPlayer1
{
get
{
return _FilterPlayer1;
}
set
{
_FilterPlayer1 = value;
NotifyPropertyChanged("FilterPlayer1");
NotifyPropertyChanged("FilteredList");
}
}
private string _FilterDate = string.Empty;
public string FilterDate
{
get
{
return _FilterDate;
}
set
{
_FilterDate = value;
NotifyPropertyChanged("FilterDate");
NotifyPropertyChanged("FilteredList");
}
}
private string _FilterTournament = string.Empty;
public string FilterTournament
{
get
{
return _FilterTournament;
}
set
{
_FilterTournament = value;
NotifyPropertyChanged("FilterTournament");
NotifyPropertyChanged("FilteredList");
}
}
#region INotifyPropertyChanged Member
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
Das Viewmodel implementiert das Interface INotifyPropertyChanged und besteht im wesentlichen daraus, Spiele aus einer Originalcollection bei Veränderung der Suchparameter in eine ObservableCollection zu pumpen, welche mit der View über Data Binding verknüft ist. Außerdem reagiert das View-Model auf Benutzereingaben in die Controls des Filters.
Den Code, um die Orginalcollection zu befüllen, habe ich aus Spaß an der Freude mit F# geschrieben:
module mvvmfilter.fscharf
#light
open System.IO
type GoGame = class
val PlayerWhite: string
val PlayerBlack: string
val Date : string
val Tournament : string
val Content : string
val Result : string
new (playerWhite, playerBlack, date, tournament, result, content) =
{ PlayerWhite = playerWhite ; PlayerBlack = playerBlack; Date = date; Tournament = tournament; Result = result; Content = content;}
end
let Games = new System.Collections.Generic.List<GoGame>()
let files = Directory.GetFiles(@"C:\Users\Oliver\Downloads\FreeProGames\AllSeperated")
let extract (ls: string, ps: string) =
let indexpw1 = ls.IndexOf(ps)
if indexpw1 < 0 then
""
else
let indexpw1x = indexpw1 + 3
let indexpw2 = ls.IndexOf("]", indexpw1x)
if indexpw2 > 0 then
ls.Substring(indexpw1x, indexpw2-indexpw1x)
else
""
let newgame (gamestr: string) =
Games.Add( new GoGame(extract(gamestr,"PW["), extract(gamestr,"PB["), extract(gamestr,"DT["), extract(gamestr,"EV["), extract (gamestr, "RE["), gamestr ))
using (File.OpenText(@"C:\Users\Oliver\Downloads\FreeProGames\collection\Collection.sgf"))
(fun f->
while not f.EndOfStream do
let content = f.ReadToEnd()
let gamestrings = content.Split[|'('|]
for i = 0 to gamestrings.Length - 1 do
let gamestring = "(" + gamestrings.[i]
newgame(gamestring)
)
Ohne, das Buch Foundations of F# von Robert Pickering, das mir freundlicherweise mein Arbeitgeber auslieh, wäre ich gar nicht weitergekommen. Online-Quellen, selbst die von Microsoft sind sehr unergiebig. Dafür gab das Buch alles, was ich brauchte, her.
Im View-Model kopiere die in F# bestückte Collection in eine C#-Collection, die dann der Anwendung als Datengrundlage dient. Ich habe festgestellt, dass der Datenimport mit F# unvergleichlich viel schneller geht, wenn ich alle Spiele aus der 57 MB großen Datei hole (einige Sekunden), statt aus 40.000 einzelnen Dateien. Die Festplatte muss da ein sehr kleines, Nadelöhr darstellen. So dauert es eine gefühlte halbe Stunde:
for filepath in files do
using (File.OpenText(filepath))
(fun f ->
while not f.EndOfStream do
let line = f.ReadToEnd()
let game = new GoGame(extract(line,"PW["), extract(line,"PB["), extract(line,"DT["), extract(line,"EV["), extract (line, "RE["), line )
Games.Add(game)
)
So sieht sie dann aus, die Oberfläche:

Es gibt nicht so viele Gospieler, die außerhalb ihrer Karriere eine Bedeutung für die Nachwelt erlangt haben. Zwei Persönlichkeiten, die aber für ihren unkomplizierten Stil bekannt sind, sind Takagawa Kaku und Otake Hideo. Also lade ich mir mal ein Spiel von diesen zwei von einem NHK-Turnier herunter. Diese Turniere haben einen sehr knappen Zeitrahmen, so dass schon gar keine Zeit existiert, um sich komplizierte Varianten auszudenken. Schnelle Spiele könnten also durch gute Form beeindrucken. Das Nachlegen der ersten Partie hat einigen Spaß bereitet:
