I have been working with WPF both for fun and for a job for about 2 years now, and have followed the evolution of WPF patterns and am luckly enough to be close to some of the biggest movers and shakers in the WPF world. It seems to be the majority of the smart WPF folks out there have settled for a pattern called ModelView-ViewModel (MVVM for short). This pattern allows a view to be abstracted to a ViewModel, where by the View is passive an simply binds (using WPF Binding) to associated ViewModel(s). The beuty of the pattern is that if you have done the whole MVVM thing correctly you should be able to test your UI without the need for the actual UI. You can test the ViewModels. In fact you should be able to examine the ViewModels for an application without even seeing the View(s) and know what is going on.
Now the holy grail of MVVM WPF development, would be to have no code behind
at all, and have the View simply bind to a ViewModel. Unfortunately there are
always some things that prevent the beauty of this. For example how does a ViewModel
show a MessageBox, or disable a control. It is quite a challenge, but luckily
WPF provides such help, by the use of the routed commanding, which allows ViewModels
to have commands which the UI controls that support the ICommand
interface to run. So what about MessageBoxes/Dialogs (such as OpenFileDialog/SaveFileDialog
etc etc) and things like that, how does a ViewModel deal with that.
Well it is not that easy using WPF out of the box, you need to put some effort into it, if you do not wich to end up with a slew of untestable code behind. Believe me, this can easily happen, and still happens to me on occassions, and it makes me really mad, as I am breaking what I consider to be a nice pattern. GRRR
So you either have to do quite a birt of custom work yourelf, or maybe let someone else put the efforts in and use their work. As luck would have it one of my WPF Disciples buddies Bill Kempf has done some nice work on a cool MVVM WPF framework that he is calling Onyx. Bill is not finished with the Onyx framework, and it is evolving, but I just wanted to give it some exposure, and see what it is capable of right now.
So in this article I am going to talk about what Bills rather nifty MVVM WPF framework can do. So if this interests you please read on.
Ok so what does the demo app look like, well I did want to do something quite
dull, so it would be clear which parts were the Onyx
framework, and which bits were not, but I just couldn't help myself, and had
to come up with something that pleased me. So I came up with a little 3D app
that lets you view some of the WPF
Disciples in 3D space, and flip the 3D to show a larger image of your chosen
WPF Disciples,
and it will also show you their blog using the .NET 3.5 WebBrowser
control.
So here is a screen shot or 6

STEP1 : Click the main button at the top

STEP2 : Pick the Disciples.xml file from the DEBUG folder of where you downloaded the code to

STEP3 : Enjoy the 3D Animation

STEP4 : Pick someone

STEP5 : Wait for slide in with the chosen persons blog

STEP6 : Read their blog in you want to.
Now I am acutely aware that this is a pretty useless app, and it is, I am under no illusions about that. The demo app is not the point, it is simple demo of what is really important, and that is the actual Onyx framework, so this is what this article is all about really. I think it is my job to try and spread the word about something that will help you, and I think the Onyx framework will help you. Good one Bill. Ok so now let's get on with the main thrust of the article shall we.
The only thing you really need to run this code is the Onyx Dll, and .NET3.5 SP1. Which is included in the downloaded code, but if you want to dive into the guts of it you will also needs Bills other opensource project Specificity, which Onyx uses for testing. That is also worth a look. It is not often I am bowled over by someones code, but I really like both of Bills open source projects.
This section will go into how Onyx helps you out in your day to day WPF MVVM development. It it worth pointing out that I am not the author of Onyx, so anything I mention here is by way of me looking through the source code that Bill wrote.
One requirement Onyx does have is that you MUST associate a particular View with its associated ViewModel using an Attached DependencyProperty. This is easily done as follows:
<Window x:Class="FlipTile3D.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:onyx="http://schemas.onyx.com/2009/fx/presentation"
xmlns:local="clr-namespace:FlipTile3D"
onyx:View.Model="{x:Type local:MainWindowViewModel}">
So what is actually happening here. Well quite a lot actually. The onyx:View.Model
attached DP is declared as such
public static readonly DependencyProperty ModelProperty =
DependencyProperty.RegisterAttached(
"Model",
typeof(object),
typeof(View),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, OnModelChanged, CoerceModel));
/// <summary>
/// Called when the <see cref="ModelProperty"/> attached dependency property is
/// changed on the <paramref name="source"/>.
/// </summary>
/// <param name="source">The source object.</param>
/// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/>
/// instance containing the event data.</param>
private static void OnModelChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if (source.AsFrameworkObject() == null)
{
return;
}
object model = e.NewValue;
if (model == null)
{
return;
}
if (DependencyPropertyHelper.GetValueSource(source, ModelProperty).
BaseValueSource == BaseValueSource.Local &&
DependencyPropertyHelper.GetValueSource(source, FrameworkElement.DataContextProperty).
BaseValueSource != BaseValueSource.Local)
{
source.SetValue(FrameworkElement.DataContextProperty, model);
}
}
What Onyx does behind the scenes when you set the onyx:View.Model
Attached DP changes is that the associated View (the source of the onyx:View.Model
Attached DP) will have its DataContext property set to the ViewModel that is
set the onyx:View.Model Attached DP value to.
So this one property is enough to set up all the binding between the View and its associated ViewModel.
Onyx makes use of RoutedCommands which are as standard WPF mechanisms. What RoutedCommands allow you to do is seperate the command and the implementation. For example it is possible to have a ViewModel expose RoutedCommands which are then used to bind to by the View, and the command implementation will then be done in the ViewModel, again allowing for clean testable code.
Here is how Onyx works with commands, all you have to do is expose a Command and define the command implementation within your ViewModel:
public class MainWindowViewModel : ViewModel
{
#region Ctor
/// <summary>
/// Initializes a new instance of the <see cref="MainWindowViewModel"/> class.
/// </summary>
/// <param name="view">The view to associate with this instance.</param>
public MainWindowViewModel(View view)
: base(view)
{
this.CommandBindings.Add(new CommandBinding(OpenDisciplesXMLFileCommand,
this.OpenDisciplesXMLFileExecuted,
new CanExecuteRoutedEventHandler(this.OpenDisciplesXMLFileCanExecute)));
}
#endregion
#region Commands
/// <summary>
/// The OpenDisciplesXMLFile routed command.
/// </summary>
private static readonly RoutedUICommand cmdOpenDisciplesXMLFile =
new System.Windows.Input.RoutedUICommand(
"Open Disciples XML File",
"OpenDisciplesXMLFileCommand",
typeof(MainWindowViewModel));
/// <summary>
/// Gets the OpenDisciplesXMLFile command.
/// </summary>
/// <value>The OpenDisciplesXMLFile command.</value>
public static RoutedUICommand OpenDisciplesXMLFileCommand
{
get { return cmdOpenDisciplesXMLFile; }
}
/// <summary>
/// Determines if the OpenDisciplesXMLFileCommand can Execute
/// </summary>
private void OpenDisciplesXMLFileCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
//Allow command to run
e.CanExecute = true;
}
/// <summary>
/// Handles the execution of the <see cref="MainWindow.OpenDisciplesXMLFile"/> command.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="args">The <see cref="System.Windows.RoutedEventArgs"/>
/// instance containing the event data.</param>
private void OpenDisciplesXMLFileExecuted(object sender, RoutedEventArgs args)
{
//DO SOMETHING
//DO SOMETHING
}
}
So all we then need to do is wire up the View to the exposed ViewModel commands. Here is an example.
<Button x:Name="btnOpenFile"
Command="local:MainWindowViewModel.OpenDisciplesXMLFileCommand"/>
That is it. This means the ViewModel has the actual command and its implementation, and the View simply binds to these exposed commands. This also enables Unit testing, where by the unit tests can execute the ViewModel commands something like:
ViewModel x = new ViewModel();
x.SomeCommand.Execute();
At its heart Onyx is a ServiceProvider. Internally it registers new services,
which can then be fetch using the Microsoft System.IServiceProvider
interface, which is defined as follows:
namespace System
{
// Summary:
// Defines a mechanism for retrieving a service object; that is, an object that
// provides custom support to other objects.
public interface IServiceProvider
{
// Summary:
// Gets the service object of the specified type.
//
// Parameters:
// serviceType:
// An object that specifies the type of service object to get.
//
// Returns:
// A service object of type serviceType. -or- null if there is no service object
// of type serviceType.
object GetService(Type serviceType);
}
}
So what happens is that the Onyx View object is used to expose its services,
which translated internally to calling the System.IServiceProvider.GetService()
method. Here is how this is done within the Onyx View object
/// <summary>
/// Gets the service object of the specified type.
/// </summary>
/// <param name="serviceType">An object that specifies the type of service object to get.</param>
/// <returns>
/// A service object of type <paramref name="serviceType"/>.
/// -or-
/// null if there is no service object of type <paramref name="serviceType"/>.
/// </returns>
/// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
public object GetService(Type serviceType)
{
if (serviceType == null)
{
throw new ArgumentNullException(Reflect.GetField(() => serviceType).Name);
}
return this.serviceContainer.GetService(serviceType);
}
So now we know that we can obtain services for a View. So what sort of services does Onyx provide. Well in order to understand that we need to look into how the Onyx View object gets created in the first place, here is what happens:
/// <summary>
/// Gets the view.
/// </summary>
/// <param name="source">The source.</param>
/// <returns>
/// The <see cref="View"/> associated with the <paramref name="source"/>.
/// </returns>
/// <remarks>
/// If there currently is no <see cref="View"/> associated with the <paramref name="source"/>
/// then a new <see cref="View"/> is created.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
public static View GetView(DependencyObject source)
{
if (source == null)
{
throw new ArgumentNullException(Reflect.GetField(() => source).Name);
}
View view = (View)source.GetValue(ViewProperty);
if (view == null)
{
view = new View(source);
source.SetValue(ViewProperty, view);
IServiceRegistry container = View.ServiceRegistryActivator.CreateServiceRegistry();
View.ViewCreated(view, new ViewCreatedEventArgs(view.ViewElement, container));
container.RegisterServicesFor(view.ViewElement);
view.serviceContainer = container.CreateServiceProvider();
}
return view;
}
It can be seen that this method 1st examines the Onyx View object to see if it needs to be instantiated, and if the Onyx View object does need creating, that is when all the services are registered. So this brings up back to the question of what services are registered and will be available. The key to this lies in looking at the way the ViewCreated event works. Lets see the resultant code that is run when the Onyx View is created.
/// <summary>
/// Called when a <see cref="View"/> is created.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="ViewCreatedEventArgs"/>
/// instance containing the event data.</param>
/// <remarks>
/// Adds common framework services to the <see cref="View"/>.
/// </remarks>
private static void OnViewCreated(object sender, ViewCreatedEventArgs e)
{
UIElement uiElement = e.ViewElement as UIElement;
if (uiElement != null)
{
UIElementServices services = new UIElementServices(uiElement);
services.AddServices(e.ServiceContainer);
}
IFrameworkObject frameworkObject = e.ViewElement.AsFrameworkObject();
if (frameworkObject != null)
{
CommonFrameworkElementServices services =
new CommonFrameworkElementServices(frameworkObject);
services.AddServices(e.ServiceContainer);
}
}
It can be seen that Onyx uses a UIElementServices object, and a CommonFrameworkElementServices object to add services from. So let us examine those 2 Onyx objects.
internal class UIElementServices : ServicesBase, ICommandTarget,
IFocusSuggested, IDisplayMessage, ICommonDialogProvider
This Onyx object provides the following services:
internal class CommonFrameworkElementServices : ServicesBase, IElementLifetime
This Onyx object provides the following services:
For now all that is important to know is that the Onyx View object exposes services, and that you are able to use these services from a ViewModel. I will now carry on and show you how to use some typical Onyx services.
One of the things that catches people when they are trying to create a typical WPF MVVM app is that they get part way there, and then something goes wrong in the ViewModel and they need to tell the user about this. Oh crap. This typically means showing a MessageBox to the user. So you could either create a new MessageBox showing interface on the View and get the ViewModel to talk to the View using this new interface, Or you ingore the fact that your ViewModel should be UI agnostic, and just let it reference the nessecary Dlls, and have the ViewModel show a MessageBox to the user.
Both these approaches do work, but the 2nd one, just doesn't feel right. The specific interface approach sounds nicer. Luckily Onyx caters for this approach quite nicely.
To show a MessageBox to the user from the ViewModel we simply obtain the correct type of service (View implemented interface), and do something with this obtained service within our ViewModel.
Onyx exposes a MessageBox service as IDisplayMessage, which can
be used as follows:
var messageBoxService = this.View.GetService<IDisplayMessage>();
messageBoxService.ShowMessage("Welcome to a small Onyx Demo.\r\n\r\n " +
"Please pick the Disciples.xml file from the debug directory","Onyx demo",
MessageBoxButton.OK,MessageBoxImage.Information);
Simple, and clean.
Showing common dialogs such as OpenFileDialog/SaveFileDialog etc etc, are another real concern, that could lead to problems, as these are typically done in code behind, which is something we are trying to avoid.
Again Onyx handles these with ease. Lets have a look at what Onyx caters for
this. Within Onyx there is a class called UIElementServices that
is used to provide UI services we touched on this earlier), recall it looked
like this:
internal class UIElementServices : ServicesBase, ICommandTarget,
IFocusSuggested, IDisplayMessage, ICommonDialogProvider
It can be seen that one of the Interfaces that the UIElementServices
class implements is ICommonDialogProvider which looks like this:
namespace Onyx.Windows
{
/// <summary>
/// Provides factory methods for creating common dialogs.
/// </summary>
public interface ICommonDialogProvider
{
/// <summary>
/// Creates the open file dialog.
/// </summary>
/// <returns>
/// A newly created <see cref="IOpenFileDialog"/> instance.
/// </returns>
IOpenFileDialog CreateOpenFileDialog();
/// <summary>
/// Creates the save file dialog.
/// </summary>
/// <returns>
/// A newly created <see cref="ISaveFileDialog"/> instance.
/// </returns>
ISaveFileDialog CreateSaveFileDialog();
}
}
So it can be seen that Onyx currently caters for OpenFileDialog
and SaveFileDialog. So how do we use a OpenFileDialog
and SaveFileDialog from our ViewModel, well it is as easy as this:
var openFileDialogService = this.View.GetService<ICommonDialogProvider>().CreateOpenFileDialog();
openFileDialogService.InitialDirectory = System.IO.Directory.GetCurrentDirectory();
openFileDialogService.Filter = "XML files (*.xml)|*.xml;|All files (*.*)|*.*";
if (openFileDialogService.ShowDialog() == true)
{
string fileName = openFileDialogService.FileName;
....
....
....
....
}
A typical ViewModel will contain properties that the View will bind to. In
order for a View to receive changes in bound values the ViewModel will typically
implment the INotifyPropertyChanged interface (When implemented,
the interface communicates to a bound control the property changes on a object)
, which is usually implmented like this:
// This class implements a simple customer type
// that implements the IPropertyChange interface.
public class DemoCustomer : INotifyPropertyChanged
{
private string customerName = String.Empty;
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
// The constructor is private to enforce the factory pattern.
private DemoCustomer()
{
companyNameValue = "no data";
}
// This is the public factory method.
public static DemoCustomer CreateNewCustomer()
{
return new DemoCustomer();
}
public string CompanyName
{
get {return this.companyNameValue;}
set
{
if (value != this.companyNameValue)
{
this.companyNameValue = value;
NotifyPropertyChanged("CompanyName");
}
}
}
}
Where binding changes are notified by the use of the INotifyPropertyChanged
interfaces NotifyPropertyChanged(String propertyName) method. The
problem with the standard implementation is that it uses magic strings in the
code. Now there is a potential for these magical strings to get missed when
refactoring, or the magic strings could be wrongly cased, or incorrectly spelt,
which would mean that the associated bound object would miss a notification
of a change in the bound object properties.
Lately there has been many people working with creating static reflection lookups,
which is typically done using LINQ Expression trees. Onyx is no different, it
also uses LINQ Expression trees to assist with the INotifyPropertyChanged
interface. What this means is that there are no magic strings. Let us see an
example:
public List DiscipleInfos
{
get { return discipleInfos; }
set
{
this.discipleInfos = value;
OnPropertyChanged(() => DiscipleInfos);
}
}
Can you see that we are using a LINQ Expression here (which if you dived into
Onyx you would see is declared as an Expression<Func<T>>
expression. Here is what happens within Onyx. As previously stated Onyx uses
a LINQ Expression tree, and does some static reflection on that to work out
the property that is forming part of the Expression tree, and then it fires
the normal INotifyPropertyChanged interface NotifyPropertyChanged(String
propertyName) method.
/// <summary>
/// Called when a property changes, using static reflection to determine the property.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="expression">Expression that accesses the property that changed.</param>
protected void OnPropertyChanged<T>(Expression<Func<T>> expression)
{
this.OnPropertyChanged(Reflect.GetProperty(expression).Name);
}
/// <summary>
/// Called when the property <paramref name="propertyName"/> has changed.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
Can you in the protected void OnPropertyChanged<T>(Expression<Func<T>>
expression) method that there is a Reflect.GetProperty() method call.
Let us examine that in a bit more detail. This is as follows:
//-----------------------------------------------------------------------------
// <copyright file="Reflect.cs" company="William E. Kempf">
// Copyright (c) William E. Kempf.
// </copyright>
//-----------------------------------------------------------------------------
namespace Onyx.Reflection
{
using System;
using System.Linq.Expressions;
using System.Reflection;
/// <summary>
/// Performs static reflection.
/// </summary>
public static class Reflect
{
/// <summary>
/// Gets a <see cref="MemberInfo"/> using static reflection.
/// </summary>
/// <param name="expression">An expression that uses member access.</param>
/// <returns>
/// A <see cref="MemberInfo"/> instance for the member accessed in the
/// <paramref name="expression"/>.
/// </returns>
public static MemberInfo GetMember(Expression<Action> expression)
{
if (expression == null)
{
throw new ArgumentNullException(
GetMember(() => expression).Name);
}
return GetMemberInfo(expression as LambdaExpression);
}
/// <summary>
/// Gets a <see cref="MemberInfo"/> using static reflection.
/// </summary>
/// <typeparam name="T">The type of the member accessed in the
/// <paramref name="expression"/>.</typeparam>
/// <param name="expression">An expression that uses member access.</param>
/// <returns>
/// A <see cref="MemberInfo"/> instance for the member accessed in the
/// <paramref name="expression"/>.
/// </returns>
public static MemberInfo GetMember<T>(Expression<Func<T>> expression)
{
if (expression == null)
{
throw new ArgumentNullException(
GetMember(() => expression).Name);
}
return GetMemberInfo(expression as LambdaExpression);
}
/// <summary>
/// Gets a <see cref="MethodInfo"/> using static reflection.
/// </summary>
/// <param name="expression">An expression that uses member access.</param>
/// <returns>
/// A <see cref="MethodInfo"/> instance for the member accessed in the
/// <paramref name="expression"/>.
/// </returns>
public static MethodInfo GetMethod(Expression<Action> expression)
{
MethodInfo method = GetMember(expression) as MethodInfo;
if (method == null)
{
throw new ArgumentException(
"Not a method call expression", GetMember(() => expression).Name);
}
return method;
}
/// <summary>
/// Gets a <see cref="PropertyInfo"/> using static reflection.
/// </summary>
/// <typeparam name="T">The type of the member accessed in the
/// <paramref name="expression"/>.</typeparam>
/// <param name="expression">An expression that uses member access.</param>
/// <returns>
/// A <see cref="PropertyInfo"/> instance for the member accessed in the
/// <paramref name="expression"/>.
/// </returns>
public static PropertyInfo GetProperty<T>(Expression<Func<T>> expression)
{
PropertyInfo property = GetMember(expression) as PropertyInfo;
if (property == null)
{
throw new ArgumentException(
"Not a property expression", GetMember(() => expression).Name);
}
return property;
}
/// <summary>
/// Gets a <see cref="FieldInfo"/> using static reflection.
/// </summary>
/// <typeparam name="T">The type of the member accessed in the
/// <paramref name="expression"/>.</typeparam>
/// <param name="expression">An expression that uses member access.</param>
/// <returns>
/// A <see cref="FieldInfo"/> instance for the member accessed in the
/// <paramref name="expression"/>.
/// </returns>
public static FieldInfo GetField<T>(Expression<Func<T>> expression)
{
FieldInfo field = GetMember(expression) as FieldInfo;
if (field == null)
{
throw new ArgumentException(
"Not a field expression", GetMember(() => expression).Name);
}
return field;
}
internal static MemberInfo GetMemberInfo(LambdaExpression lambda)
{
if (lambda == null)
{
throw new ArgumentNullException(
GetMember(() => lambda).Name);
}
MemberExpression memberExpression = null;
if (lambda.Body.NodeType == ExpressionType.Convert)
{
memberExpression = ((UnaryExpression)lambda.Body).Operand as MemberExpression;
}
else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
{
memberExpression = lambda.Body as MemberExpression;
}
else if (lambda.Body.NodeType == ExpressionType.Call)
{
return ((MethodCallExpression)lambda.Body).Method;
}
if (memberExpression == null)
{
throw new ArgumentException(
"Not a member access", GetMember(() => lambda).Name);
}
return memberExpression.Member;
}
}
}
Bill talks a bit more about this over at his blog : Reflecting on Code : Static reflection. Its all the rage.
One of the very nice things that Onyx provides is that is exposes some nice methods (which are raised as results of the associated View raising its own events) that one would expect to find on a typical Window, such as
protected virtual void OnViewInitialized()protected virtual void OnViewUnloaded()protected virtual void OnViewLoaded()Where Oyxn does the following to create these methods
protected ViewModel(View view)
{
this.view = view;
IElementLifetime lifetime = this.view.GetService<IElementLifetime>();
if (lifetime != null)
{
lifetime.Initialized += (s, e) => this.OnViewInitialized();
lifetime.Unloaded += (s, e) => this.OnViewUnloaded();
lifetime.Loaded += (s, e) => this.OnViewLoaded();
}
}
IElementLifetime is just an interface that Onyx uses to make a
further abstraction for the View to be only these 3 methods.
So what does this mean to you if you are working in your ViewModel. Well it means that we can simply override these methods and do things within ouor ViewModel when one of these events occurs within the View that the ViewModel is abstracting.
Here is an example, that the attached demo code uses :
/// <summary>
/// Occurs when the associated View is loaded
/// </summary>
protected override void OnViewLoaded()
{
base.OnViewLoaded();
//run messagebox on Background give view some time for view to render properly
base.View.ViewElement.Dispatcher.BeginInvoke((Action)delegate
{
var messageBoxService = this.View.GetService<IDisplayMessage>();
messageBoxService.ShowMessage("Welcome to a small Onyx Demo.\r\n\r\n " +
"Please pick the Disciples.xml file from the debug directory","Onyx demo",
MessageBoxButton.OK,MessageBoxImage.Information);
}, DispatcherPriority.Background);
}
In this section of code, I am using Onyx to locate a service called IDisplayMessage
which can be used to display a MessageBox to the user via the associated View.
Also of note here is that I am using the Dispatcher associated
with the View, which again is exposed very easily in Onyx, you simply have to
use the base.View.ViewElement.Dispatcher to obtain the correct
Views Dispatcher.
I mentioned to Bill that I thought there was something missing which was the ability to open new actual WPF windows from a ViewModel and the whole issue of navigation. Bill is aware of this, but it is a complex subject, however it is something that Bill mentioned would appear in Onyx within the future. So all I can about that one is stay tuned and keep checking the Onyx codeplex site.
I would just like to close by stating that if you are doing an sort of WPF development, it will be well worth your while looking into Onyx, which as I stated is still a work in progress, but it is a good work in progress, that I have found to be very intuitive, and it actually caters for most nasties that would otherwise be a headache. Nice one Bill, it is really very very cool. If I wasn't so far into my current WPF project at work, I would be using this framework for sure, and certainly will for future projects.
Oh and another thing, of late the WPF Disciples have all been having heavy discussions about a new codeplex site for the commuity. This site is available at :
http://www.codeplex.com/mvvmref
Here is what this site will be aiming to cover
M-V-VM Reference Application
Project Description
Community
created reference application for M-V-VM frameworks to use for
demonstration purposes, similar in concept to Pet Shop for web
frameworks.
This project isn't going to give any
solutions for following the Model-View-ViewModel pattern.
What it is going to do is provide a sample application that does "typical" things that cause problems when following an M-V-VM architecture. Then, given this reference application and supporting documentation, other implementations can be made using specific M-V-VM libraries.
Those other implementations won't be part of this project, though we'll provide links to them.
Instead, they'll be reference implementations provided by the specific M-V-VM library's author(s) or interested third parties. This makes it easier on the library developers, since they don't have to dream up their own reference implementation sample. It benefits the users, since they can compare various implementations using "competing" libraries, and they can learn "best practices" of how to use a given library. Again, if you're at all familiar, this is "Pet Shop" for WPF and Silverlight.
Check it out
MVVM Exemplar / Reference : WPF Disciples
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||