using System;
using System.Collections.Generic;
using System.Text;
namespace ModelSample
{
/// <summary>
/// Interface for a provider of stock quotes.
/// </summary>
public interface IStockQuoteProvider
{
/// <summary>
/// Get a quote. This function may block on slow operations like hitting the network.
/// </summary>
/// <param name="symbol">The stock symbol.</param>
/// <param name="quote">The quote.</param>
/// <returns>Whether we were able to get a quote.</returns>
bool TryGetQuote(string symbol, out double quote);
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace ModelSample
{
/// <summary>
/// Mock IStockQuoteProvider that alwyas returns 100.
/// </summary>
public class MockQuoteProvider : IStockQuoteProvider
{
public bool TryGetQuote(string symbol, out double quote)
{
quote = 100.0;
return true;
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Windows.Input;
namespace ModelSample
{
/// <summary>
/// View model for the portfolio view
/// </summary>
public class PortfolioViewModel
{
public PortfolioViewModel(IStockQuoteProvider quoteProvider)
{
_quoteProvider = quoteProvider;
_stockModels = new ObservableCollection<StockModel>();
_stockModels.Add(new StockModel("MSFT", _quoteProvider));
_addCommand = new AddCommand(this);
_removeCommand = new RemoveCommand(this);
}
/// <summary>
/// The list of StockModels for the page.
/// </summary>
public ObservableCollection<StockModel> Stocks
{
get { return _stockModels; }
}
/// <summary>
/// A CommandModel for Add. The command parameter should be the symbol to add.
/// </summary>
public CommandModel AddCommandModel
{
get { return _addCommand; }
}
/// <summary>
/// A CommandModel for Remove. The command parameter should be the StockModel to remove.
/// </summary>
public CommandModel RemoveCommandModel
{
get { return _removeCommand; }
}
/// <summary>
/// Private implementation of the Add Command
/// </summary>
private class AddCommand : CommandModel
{
public AddCommand(PortfolioViewModel viewModel)
{
_viewModel = viewModel;
}
public override void OnQueryEnabled(object sender, CanExecuteRoutedEventArgs e)
{
string symbol = e.Parameter as string;
e.CanExecute = (!string.IsNullOrEmpty(symbol));
e.Handled = true;
}
public override void OnExecute(object sender, ExecutedRoutedEventArgs e)
{
string symbol = e.Parameter as string;
_viewModel._stockModels.Add(new StockModel(symbol, _viewModel._quoteProvider));
}
private PortfolioViewModel _viewModel;
}
/// <summary>
/// Private implementation of the Remove command
/// </summary>
private class RemoveCommand : CommandModel
{
public RemoveCommand(PortfolioViewModel viewModel)
{
_viewModel = viewModel;
}
public override void OnQueryEnabled(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = e.Parameter is StockModel;
e.Handled = true;
}
public override void OnExecute(object sender, ExecutedRoutedEventArgs e)
{
_viewModel._stockModels.Remove(e.Parameter as StockModel);
}
private PortfolioViewModel _viewModel;
}
private ObservableCollection<StockModel> _stockModels;
private CommandModel _addCommand;
private CommandModel _removeCommand;
private IStockQuoteProvider _quoteProvider;
}
}
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Windows.Threading;
namespace ModelSample
{
public class StockModel : DataModel
{
public StockModel(string symbol, IStockQuoteProvider quoteProvider)
{
_symbol = symbol;
_quoteProvider = quoteProvider;
}
protected override void OnActivated()
{
VerifyCalledOnUIThread();
base.OnActivated();
_timer = new DispatcherTimer(DispatcherPriority.Background);
_timer.Interval = TimeSpan.FromMinutes(5);
_timer.Tick += delegate { ScheduleUpdate(); };
_timer.Start();
ScheduleUpdate();
}
protected override void OnDeactivated()
{
VerifyCalledOnUIThread();
base.OnDeactivated();
_timer.Stop();
_timer = null;
}
private void ScheduleUpdate()
{
VerifyCalledOnUIThread();
// Queue a work item to fetch the quote
if (ThreadPool.QueueUserWorkItem(new WaitCallback(FetchQuoteCallback)))
{
this.State = ModelState.Fetching;
}
}
/// <summary>
/// Gets the stock symbol.
/// </summary>
public string Symbol
{
get { return _symbol; }
}
/// <summary>
/// Gets the current quote for the stock. Only valid if State == Active.
/// </summary>
public double Quote
{
get
{
VerifyCalledOnUIThread();
return _quote;
}
private set
{
VerifyCalledOnUIThread();
if (_quote != value)
{
_quote = value;
SendPropertyChanged("Quote");
}
}
}
/// <summary>
/// Callback on background thread to fecth quote.
/// </summary>
private void FetchQuoteCallback(object state)
{
double fetchedQuote;
if (_quoteProvider.TryGetQuote(_symbol, out fetchedQuote))
{
this.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new ThreadStart(delegate
{
this.Quote = fetchedQuote;
this.State = ModelState.Valid;
}));
}
else
{
this.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new ThreadStart(delegate
{ this.State = ModelState.Invalid; }));
}
}
private string _symbol;
private double _quote;
private IStockQuoteProvider _quoteProvider;
private DispatcherTimer _timer;
}
}
<Window x:Class="ModelSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ModelSample" Height="300" Width="300"
>
<Grid>
<ContentControl x:Name="_content" />
</Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace ModelSample
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
_content.Content = new PortfolioViewModel(new MockQuoteProvider());
}
}
}
<Application x:Class="ModelSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ModelSample"
StartupUri="Window1.xaml"
>
<Application.Resources>
<!-- Data Template for individual stocks -->
<DataTemplate DataType="{x:Type local:StockModel}">
<StackPanel Orientation="Horizontal" local:ActivateModel.Model="{Binding}">
<TextBlock Text="{Binding Symbol}" Width="100"/>
<TextBlock Text="{Binding Quote}" />
</StackPanel>
</DataTemplate>
<!-- Definition of Portfolio view -->
<DataTemplate DataType="{x:Type local:PortfolioViewModel}">
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Top" TextAlignment="Center">Portfolio View</TextBlock>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<TextBox Name="AddSymbol" Width="100" />
<Button Command="{Binding AddCommandModel.Command}"
CommandParameter="{Binding Path=Text,ElementName=AddSymbol}"
local:CreateCommandBinding.Command="{Binding AddCommandModel}"
>
Add
</Button>
<Button Margin="50,0,0,0"
Command="{Binding RemoveCommandModel.Command}"
CommandParameter="{Binding Path=SelectedItem,ElementName=StockList}"
local:CreateCommandBinding.Command="{Binding RemoveCommandModel}"
>
Remove
</Button>
</StackPanel>
<ListBox Name="StockList" ItemsSource="{Binding Stocks}" />
</DockPanel>
</DataTemplate>
</Application.Resources>
</Application>
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Input;
namespace ModelSample
{
/// <summary>
/// Model for a command
/// </summary>
public abstract class CommandModel
{
public CommandModel()
{
_routedCommand = new RoutedCommand();
}
/// <summary>
/// Routed command associated with the model.
/// </summary>
public RoutedCommand Command
{
get { return _routedCommand; }
}
/// <summary>
/// Determines if a command is enabled. Override to provide custom behavior. Do not call the
/// base version when overriding.
/// </summary>
public virtual void OnQueryEnabled(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
e.Handled = true;
}
/// <summary>
/// Function to execute the command.
/// </summary>
public abstract void OnExecute(object sender, ExecutedRoutedEventArgs e);
private RoutedCommand _routedCommand;
}
}
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Input;
namespace ModelSample
{
/// <summary>
/// Attached property that can be used to create a binding for a CommandModel. Set the
/// CreateCommandBinding.Command property to a CommandModel.
/// </summary>
public static class CreateCommandBinding
{
public static readonly DependencyProperty CommandProperty
= DependencyProperty.RegisterAttached("Command", typeof(CommandModel), typeof(CreateCommandBinding),
new PropertyMetadata(new PropertyChangedCallback(OnCommandInvalidated)));
public static CommandModel GetCommand(DependencyObject sender)
{
return (CommandModel)sender.GetValue(CommandProperty);
}
public static void SetCommand(DependencyObject sender, CommandModel command)
{
sender.SetValue(CommandProperty, command);
}
/// <summary>
/// Callback when the Command property is set or changed.
/// </summary>
private static void OnCommandInvalidated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
// Clear the exisiting bindings on the element we are attached to.
UIElement element = (UIElement)dependencyObject;
element.CommandBindings.Clear();
// If we're given a command model, set up a binding
CommandModel commandModel = e.NewValue as CommandModel;
if (commandModel != null)
{
element.CommandBindings.Add(new CommandBinding(commandModel.Command, commandModel.OnExecute, commandModel.OnQueryEnabled));
}
// Suggest to WPF to refresh commands
CommandManager.InvalidateRequerySuggested();
}
}
}
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Threading;
namespace ModelSample
{
/// <summary>
/// Base class for data models. All public methods must be called on the UI thread only.
/// </summary>
public class DataModel : INotifyPropertyChanged
{
public DataModel()
{
_dispatcher = Dispatcher.CurrentDispatcher;
this.State = ModelState.Invalid;
}
/// <summary>
/// Possible states for a DataModel.
/// </summary>
public enum ModelState
{
Invalid, // The model is in an invalid state
Fetching, // The model is being fetched
Valid // The model has fetched its data
}
/// <summary>
/// Is the model active?
/// </summary>
public bool IsActive
{
get
{
VerifyCalledOnUIThread();
return _isActive;
}
private set
{
VerifyCalledOnUIThread();
if (value != _isActive)
{
_isActive = value;
SendPropertyChanged("IsActive");
}
}
}
/// <summary>
/// Activate the model.
/// </summary>
public void Activate()
{
VerifyCalledOnUIThread();
if (!_isActive)
{
this.IsActive = true;
OnActivated();
}
}
/// <summary>
/// Override to provide behavior on activate.
/// </summary>
protected virtual void OnActivated()
{
}
/// <summary>
/// Deactivate the model.
/// </summary>
public void Deactivate()
{
VerifyCalledOnUIThread();
if (_isActive)
{
this.IsActive = false;
OnDeactivated();
}
}
/// <summary>
/// Override to provide behavior on deactivate.
/// </summary>
protected virtual void OnDeactivated()
{
}
/// <summary>
/// PropertyChanged event for INotifyPropertyChanged implementation.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged
{
add
{
VerifyCalledOnUIThread();
_propertyChangedEvent += value;
}
remove
{
VerifyCalledOnUIThread();
_propertyChangedEvent -= value;
}
}
/// <summary>
/// Gets or sets current state of the model.
/// </summary>
public ModelState State
{
get
{
VerifyCalledOnUIThread();
return _state;
}
set
{
VerifyCalledOnUIThread();
if (value != _state)
{
_state = value;
SendPropertyChanged("State");
}
}
}
/// <summary>
/// The Dispatcher associated with the model.
/// </summary>
public Dispatcher Dispatcher
{
get { return _dispatcher; }
}
/// <summary>
/// Utility function for use by subclasses to notify that a property has changed.
/// </summary>
/// <param name="propertyName">The name of the property.</param>
protected void SendPropertyChanged(string propertyName)
{
VerifyCalledOnUIThread();
if (_propertyChangedEvent != null)
{
_propertyChangedEvent(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// Debugging utility to make sure functions are called on the UI thread.
/// </summary>
[Conditional("Debug")]
protected void VerifyCalledOnUIThread()
{
Debug.Assert(Dispatcher.CurrentDispatcher == this.Dispatcher, "Call must be made on UI thread.");
}
private ModelState _state;
private Dispatcher _dispatcher;
private PropertyChangedEventHandler _propertyChangedEvent;
private bool _isActive;
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Windows;
namespace ModelSample
{
/// <summary>
/// Attached property that can be used to activate a model.
/// </summary>
public static class ActivateModel
{
public static readonly DependencyProperty ModelProperty
= DependencyProperty.RegisterAttached("Model", typeof(DataModel), typeof(ActivateModel),
new PropertyMetadata(new PropertyChangedCallback(OnModelInvalidated)));
public static DataModel GetModel(DependencyObject sender)
{
return (DataModel)sender.GetValue(ModelProperty);
}
public static void SetModel(DependencyObject sender, DataModel model)
{
sender.SetValue(ModelProperty, model);
}
/// <summary>
/// Callback when the Model property is set or changed.
/// </summary>
private static void OnModelInvalidated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = (FrameworkElement)dependencyObject;
// Add handlers if necessary
if (e.OldValue == null && e.NewValue != null)
{
element.Loaded += OnElementLoaded;
element.Unloaded += OnElementUnloaded;
}
// Or, remove if necessary
if (e.OldValue != null && e.NewValue == null)
{
element.Loaded -= OnElementLoaded;
element.Unloaded -= OnElementUnloaded;
}
// If loaded, deactivate old model and activate new one
if (element.IsLoaded)
{
if (e.OldValue != null)
{
((DataModel)e.OldValue).Deactivate();
}
if (e.NewValue != null)
{
((DataModel)e.NewValue).Activate();
}
}
}
/// <summary>
/// Activate the model when the element is loaded.
/// </summary>
static void OnElementLoaded(object sender, RoutedEventArgs e)
{
FrameworkElement element = (FrameworkElement)sender;
DataModel model = GetModel(element);
model.Activate();
}
/// <summary>
/// Deactivate the model when the element is unloaded.
/// </summary>
static void OnElementUnloaded(object sender, RoutedEventArgs e)
{
FrameworkElement element = (FrameworkElement)sender;
DataModel model = GetModel(element);
model.Deactivate();
}
}
}