I've been busy with stuff, but I was able to devote some time this weekend to getting my butt kicked by MVVM. I know that it's supposed to make things easier, but how can something that is supposed to simplify things be so hard to understand? I get the concepts - separate your data from your logic from your views, but quite honestly that's easier said than done.
I separated my data into Model, ViewModel, and View folders, as shown:
And I expect to be adding more items, as I've only implemented the very basics of my app (again). Just trying to get things working and responsive, for now.
The Model contains my Expenses class with the following code:
public class Expenses : INotifyPropertyChanged {
public string Expense { get; set; }
private double _cost;
public double Cost {
get { return _cost; }
set {
_cost += value;
OnPropertyChanged("Cost");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string p) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(p));
}
}
}
So basically what my Model is - it's each key, value pair that will eventually be displayed. Like, <"Shoes", 50>. In my previous implementation the Cost was represented by an int, but why not let people put in a decimal value. When parsing the values, any values with a decimal value would be ignored and I realized that probably isn't a good thing.
I only have one ViewModel so far, my ExpenseViewModel but I'll probably add more as I continue developing this app. Here's the code:
public class ExpenseViewModel {
IsolatedStorageSettings storage = IsolatedStorageSettings.ApplicationSettings;
public static ObservableCollection<Expenses> AllExpenses = new ObservableCollection<Expenses>();
public ExpenseViewModel() { GetExpenses(); }
public void GetExpenses() {
if (storage.Count > 0) { GetSavedExpenses(); }
else { GetDefaultExpenses(); }
}
private void GetDefaultExpenses() {
AllExpenses.Add(new Expenses() { Expense = "Running Tally", Cost = 0 }));
storage.Add("Running Tally", 0);
AllExpenses.Add(new Expenses() { Expense = "Other", Cost = 0 }));
storage.Add("Other", 0);
storage.Save();
}
private void GetSavedExpenses() {
foreach (string s in storage.Keys) {
double d;
if (Double.TryParse(storage[s].ToString(), out d)) {
var expense = new Expenses() { Expense = s, Cost = d };
AllExpenses.Add(expense);
}
}
}
public void Add(string s, double f) {
AllExpenses.Add(new Expenses() { Expense = s, Cost = f});
if (!storage.Contains(s)) {
storage.Add(s, f);
storage.Save();
}
}
}
I wish there was a better way of displaying the data than loading it every single time the app starts, but that's what I have so far. I took some ideas from Microsoft's
Implementing the Model-View-ViewModel tutorial. Instead of implementing the INotifyPropertyChanged interface I used the ObservableCollection, which notifies whoever is interested that new items have been added or removed from the current list of data.
Now on to the views. Here's my ExpenseView.xaml file (with nothing added in the code-behind, yay!)
<UserControl.Resources>
<DataTemplate x:Key="MyPrettyTemplate">
<StackPanel Orientation="Horizontal"
Background="{StaticResource PhoneAccentBrush}" Margin="5">
<TextBlock Text="{Binding Expense}:
Style="{StaticResource PhoneTextSubtleStyle}"
Margin="10" Width="300" />
<TextBlock Text="$ "
Style="{StaticResource PhoneTextSubtleStyle"}
Margin="10,10,0,10" />
<TextBlock Text="{Binding Cost}"
Style="{StaticResource PhoneTextSubtleStyle}"
Margin="0,10,10,10" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<LisBox x:Name="ExpenseViewOnPage"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource MyPrettyTemplate}" />
</Grid>
MyPrettyTemplate just specifies how the items are to be presented, in ExpenseViewOnPage I give the TextBlock fields a DataContext to go on, and a DataTemplate so they can be pretty. The DataContext is my ExpenseViewModel's AllExpenses - remember the ObservableCollection<Expenses>?
Here's how it looks. For some reason I couldn't get the LongListSelector to work with a UserControl, so I used a ListBox but now the TextBlocks don't stretch out like they used to, so I'll have to play around with that:
Here's the code for my AddExpenses.xaml:
<Grid x:Name="LayoutRoot">
<StackPanel x:Name="AddExpenseViewOnPage">
<TextBox x:Name="txtInput" Width="438"
InputScope="Text" />
<TextBox x:Name="numInput" Width="438"
InputScope="Digits" Margin="0,-10,0,0" />
<Button x:Name="addBtn" Content="Add" Width="438"
Background="{StaticResource PhoneChromeBrush}"
Margin="0,-10,0,0" Click="addBtn_Click" />
</StackPanel>
</Grid>
I didn't do any of the fancy tricks I was so proud of before - greying out default values in the fields and stuff. I just want to get this working first.
Unfortunately I had to add stuff in my code-behind. I know that to deal with commands such as Save commands or Add commands and the like, you're supposed to do that in the ViewModel with a RelayCommand but I'll add that later. So for right now I have a Click handler for my button, but I plan on making a ViewModel for my AddExpenses view and implementing the Command in there.
So here it is:
public partial class AddExpenses : UserControl {
IsolatedStorageSettings storage = IsolatedStorageSettings.ApplicationSettings;
public AddExpenses() {
InitializeComponent();
}
private void addBtn_Click(object sender, RoutedEventArgs e) {
var ExpenseStorage = ExpenseViewModel.AllExpenses;
string s = txtInput.Text;
double d;
if (double.TryParse(numInput.Text, out d)) {
ExpenseStorage.Add(new Expenses() { Expense = s, Cost = d });
if (!storage.Contains(s)) {
storage.Add(s, d);
storage.Save();
}
}
}
}
Now, for the MainPage.xaml - I'd like to point out here that, in my previous app, I had made two pages for the two main components of my app - the add page and the display page. Here, I only have one page, but two UserControls for the add and the display functionality. Now it's starting to remind me a lot of the Pivot template so I'll probably just use that later.
Here's where I import the views:
xmlns:views="clr-namespace:expense_thing_test.View"
And where I bring in the views:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="'12,0,12,0">
<StackPanel>
<views:AddExpenses x:Name="AddExpenseViewOnPage" Visibility="Visible"/>
<views:ExpenseView x:Name="ExpenseViewOnPage" Visibility="Collapsed"/>
</StackPanel>
</Grid>
I have code for an app bar but it's all non-functional except for my toggle button, so I won't include it. Also, I can't explain all the technical details, but basically the app bar is a special control that you can't bind a Command to, so you can't implement pure MVVM with it. There are ways around this but as I don't even understand RelayCommand yet I'm not going to mess with that yet.
Here is my code-behind in my Mainpage class:
public partial class Mainpage : PhoneApplicationpage {
private ExpenseViewModel vm;
public Mainpage() {
InitializeComponent();
vm = new ExpenseViewModel();
ExpenseViewOnPage.DataContext = ExpenseViewModel.AllExpenses;
}
private void display_button_Click(object sender, EventArgs e) {
if (AddExpenseViewOnPage.Visibility == Visibility.Visible) {
AddExpenseViewOnPage.Visibility = Visibility.Collapsed;
ExpenseViewOnPage.Visibility = Visibility.Visible;
} else {
ExpenseViewOnPage.Visibility = Visibility.Collapsed;
AddExpenseViewOnPage.Visibility = Visibility.Visible;
}
}
}