다음을 통해 공유


Model-see, Model-do, and the Poo is Optional

Like a lot of people, I’ve developed software professionally for a lot of different environments: PC systems and embedded systems; high- and low-level languages; kernel mode, user mode, real mode, and protected mode; system services; domain controllers; bootstrappers; image processors; a debugger; a compiler; a search engine; small systems and really big systems; file systems; COM; real-time and internet time. I’ve never quite developed professionally for a mainframe, but I’ve worked on clients for mainframe services, and I sat next to a guy that wrote the services (I also sat next to a guy once who would only use a Commodore 64).

 

But I’ve been spending the last bunch of years helping to create WPF and Silverlight. So in a model/view world, I look at a lot of things from the View point of view. And from that point of view, you come to realize that all computer applications are really just data visualization applications. There’s certainly other software – services, device drivers, pi calculators (I’m still waiting for that one to respond) – but end-user applications are all about data viz.

 

Except that’s not quite the right, because you don’t just visualize data; you manipulate it too. E.g. Netflix.com lets me both see and modify my queue. And the word “data” isn’t great either, because it makes you think just of the bits stored in a database. So that got me to all applications being some form of “model visualization and manipulation”.

 

That doesn’t roll of the tongue either though. So now I just consider all applications to be a case of “model see model do”. Which makes me a code monkey.

 

 

Models, Views, and Poo

 

As for models and views, in my current View point of view, there’s always a view, and there’s optionally some amount (and maybe a large amount) of model. The view visualizes the model, and the end-user interacts with that visualization, which propagates back as changes to the model.

 

A lot of simple apps just need a view – a bunch of UI controls created in Xaml, with event handlers hooked up to some code-behind logic. But I usually end up splitting up my application logic into some model objects and a view object, with the controls in the view bound to model properties.

 

For more complex applications, you could have a more complicated view. But then you can’t change the look of your application easily, and views are more difficult to test. The solution to that is more sophisticated models. I like the Model/View/ViewModel approach (MVVM), which is similar to the Model/View/Presenter (MVP) pattern.

 

But I think it’s really useful to be able to pick the right number of layers and sophistication behind my view as is appropriate for the application; it’s all just some form of model in the end. Dr. WPF and Josh Smith talk about this too. Dr. WPF put a name on this agnosticism towards the particular model – Model/View/Poo. I like it. (There’s a joke in there somewhere about code monkeys and poo.)

 

Another thing I find interesting about the layers of models is that we continue to work to narrow the gap between the model and the view, by making the model more view-friendly:

· For example, tools generate classes with IEditableObject implementations, which enables view controls to perform cancelable updates to the model objects.

· For example, Linq makes it easier to convert data sources into objects, or to re-shape existing objects into objects more appropriate for the view.

 

Similarly, we’re making the view more model-friendly:

· For example, it’s easy to bind to any model object, or to lists of objects, or to find visualizations for model objects, etc.

· For example, the CollectionView class provides built-in support for the classic problem of synchronizing two parts of the view, such as the master list and the details pane.

 

 

 

Updates to the Model, from the View, in Xaml

 

Along the general lines of the “model do” side of things, it’s common to hook events in the view and write code to update the model, or to define commands in the model that are invoked by e.g. buttons in the view. As an experiment, I played with one way of somewhat marrying the two approaches – allow the view to phrase a model method in the form of a command.

 

The example is a view that’s bound to a File object; the view shows the file name, but as TextBox so that you can change the name. When you click the “Rename” button, it fires a command which calls the Rename method on the File object with the new name from the TextBox.

 

I.e., here’s the File object, which just has a Name property and a Rename method:

 

public class File : INotifyPropertyChanged

{

    string _name;

    public string Name

    {

        get { return _name; }

        set

        {

            _name = value;

            if (PropertyChanged != null)

                PropertyChanged(this, new PropertyChangedEventArgs("Name"));

        }

    }

    public void Rename(string newName)

    {

        // Perform the file rename here

    }

    public event PropertyChangedEventHandler PropertyChanged;

}

 

… here’s some sample data getting hooked up as the DataContext:

 

public Window1()

{

    InitializeComponent();

    DataContext = new File() { Name = "TestName" };

}

 

… and here’s the view, which has a TextBox and a Button, a command on the Button that calls the Rename method, and the argument to the Rename coming from the TextBox:

 

<StackPanel>

    <TextBox Name="_renameTextBox" Text="{Binding Name}"/>

    <Button Content="Rename">

        <Button.Command>

            <mc:MethodCommand MethodName="Rename">

                <mc:MethodArgument Value="{Binding Text, ElementName=_renameTextBox}" />

            </mc:MethodCommand>

        </Button.Command>

    </Button>

</StackPanel>

 

 

The key here is that when the Button is clicked, the MethodCommand is invoked. That MethodCommand calls the Rename method on the File object, passing in the current value of the TextBox as the newName parameter. The File object is picked up by the MethodCommand by inheriting the DataContext.

 

The rest of this post is the MethodCommand implementation. The reason I made the MethodCommand class and the Arguments property of type Freezable was so that the bindings would be fully functional (so that the DataContext would inherit, and so that ElementName bindings could work).

 

(I haven’t tested this on Commodore 64 yet.)

 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows.Input;

using System.Windows;

using System.Reflection;

using System.Collections;

using System.Windows.Data;

using System.ComponentModel;

using System.Windows.Markup;

namespace MethodCommandNS

{

    [ContentProperty("Arguments")]

    public class MethodCommand

        : Freezable, // Enable ElementName and DataContext bindings

          ICommand,

          INotifyPropertyChanged

    {

        // The name of the method to call on Invoke

        public string MethodName { get; set; }

        // When this is set, exceptions during invoke are caught, and the exception

        // is set as the Exception property

        [DefaultValue(true)]

        public bool CatchExceptions { get; set; }

        // If there is an exception during a command invoke, and CatchExceptions

        // is set, this will have the exception object.

        public Exception Exception

        {

            get { return _exception; }

            private set

            {

                _exception = value;

                FirePropertyChanged("Exception");

            }

        }

        Exception _exception;

        // This holds the arguments to be passed to the method.

        // This is Freezable so that the DataContext and ElementName bindings

        // can work correctly.

        public FreezableCollection<MethodArgument> Arguments

        {

            get { return (FreezableCollection<MethodArgument>)GetValue(ArgumentsProperty); }

        }

        public static readonly DependencyProperty ArgumentsProperty =

            DependencyProperty.Register("Arguments", typeof(FreezableCollection<MethodArgument>), typeof(MethodCommand), null);

        // This is a private DP that's used to get the inherited DataContext

        // (see it used in the MethodCommand constructor).

        private static readonly DependencyProperty ElementDataContextProperty =

            DependencyProperty.Register("ElementDataContext", typeof(object), typeof(MethodCommand), null);

        // The Target property specifies the object on which to invoke the method.

        // If this is null, invoke the method on the DataContext

        public object Target

        {

            get { return (object)GetValue(TargetProperty); }

            set { SetValue(TargetProperty, value); }

        }

        public static readonly DependencyProperty TargetProperty =

            DependencyProperty.Register("Target", typeof(object), typeof(MethodCommand), null);

        // Constructor

        public MethodCommand()

        {

            // The Arguments property is read-only and never null

            SetValue(ArgumentsProperty, new FreezableCollection<MethodArgument>());

            // Set a default Binding onto the private ElementDataContextProperty.

            // A default binding just binds to the inherited DataContext. This is how

            // MethodCommand typically gets the object on which to invoke the method.

            BindingOperations.SetBinding(

                this,

                ElementDataContextProperty,

         new Binding());

            // By default, catch exceptions that are raised by the method.

            CatchExceptions = true;

        }

        // Fire the PropertyChanged event when the Exception property changes.

        public event PropertyChangedEventHandler PropertyChanged;

        void FirePropertyChanged(string propertyName)

        {

            if (PropertyChanged != null)

            {

                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

            }

        }

        // We need to implement this method because we're a Freezzable subtype.

        protected override Freezable CreateInstanceCore()

        {

            throw new NotImplementedException();

        }

        // Implement the CanExecute members of ICommand

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)

        {

            // We'll always be enabled to be invokes.

            return true;

        }

        // ICommand.Execute implementation. This version calls ExecuteImpl, which does the

        // the actual call. But this method wraps that call in a try/finally, depending on

        // the value of CatchExceptions.

        public void Execute(object parameter)

        {

            // Clear out any exception of a former invocation.

            Exception = null;

            if (CatchExceptions)

            {

                // Invoke the method in a try/finally

                try

                {

                    ExecuteImpl(parameter);

                }

                catch (Exception e)

                {

                    // Any exceptions will likely come back as a

                    // TargetInvocationException, but the original exception

                    // is more interesting.

                    if (e is TargetInvocationException)

                    {

                        if (e.InnerException != null)

                            Exception = e.InnerException;

                  else

                            Exception = e;

                    }

                    else

                        Exception = e;

                }

            }

            // Otherwise, CatchExceptions isn't set, so just forward the call

     else

            {

                ExecuteImpl(parameter);

            }

        }

        // ExecuteImpl is where we actually invoke the method.

        void ExecuteImpl(object parameter)

        {

            // See if the Target property is set

            object target = Target;

            if (target == null)

            {

                // If not, look for an inherited DataContext

                target = GetValue(ElementDataContextProperty);

            }

            // We must have a target, either from Target or DataContext

            if (target == null)

            {

                throw new InvalidOperationException("MethodCommand target not found (must set either Target or DataContext)");

            }

            // Get the method to be called. Note that this doesn't support

            // overloaded methods, but it could be updated to do so.

            MethodInfo methodInfo = target.GetType().GetMethod(MethodName);

            if (methodInfo == null)

            {

                throw new InvalidOperationException("Method '" + MethodName + "' couldn't be found on type '" + target.GetType().Name + "'");

            }

            // Copy the Arguments to an array

            object[] arguments = new object[Arguments.Count];

            for (int i = 0; i < Arguments.Count; i++)

                arguments[i] = Arguments[i].Value;

            // Invoke the method

            methodInfo.Invoke(target, arguments);

        }

    }

    // The MethodArgument class plugs into MethodCommand.Arguments

    public class MethodArgument

        : Freezable // Enable ElementName and DataContext bindings

    {

        public MethodArgument() { }

        // The value of a method argument

        public object Value

        {

            get { return (object)GetValue(ValueProperty); }

            set { SetValue(ValueProperty, value); }

        }

        public static readonly DependencyProperty ValueProperty =

            DependencyProperty.Register("Value", typeof(object), typeof(MethodArgument), null);

        // We need to implement this method since we are a subtype of Freezable. But since

        // we don't need to support cloning, we won't implement it.

        protected override Freezable CreateInstanceCore()

        {

            throw new NotImplementedException();

        }

    }

}

Comments

  • Anonymous
    May 22, 2008
    PingBack from http://www.alvinashcraft.com/2008/05/22/dew-drop-may-22-2008/

  • Anonymous
    May 23, 2008
    Mike Hillberg has some great observations about WPF application architecture as it pertains to model interaction in his "Model See Model Do" post. I am very much in agreement wi ...

  • Anonymous
    May 23, 2008
    Mike, Great post!  This is very similar to the way that Caliburn's ActionMessage and EventMessage systems work.  I've been working with WPF in this way for quite a while and found it a very pleasant way to develop.  Caliburn also works with any event on any control type.  It can automatically determine input parameters, run code asynchronously (with callbacks), databind method return values and handle exceptions.  All these features are also available for the CanExecute scenarios, except the UI isn't limited to only disabling itself if the action isn't available.  It can also hide and collapse, as well as have any other behavior that the develop wishes to plug. That's just one of Caliburn's feature areas.  I'd love for you to check it out and give me some feedback.  The project web site is http://caliburn.tigris.org/

  • Anonymous
    April 16, 2009
    Looks like Caliburn has moved to http://www.codeplex.com/caliburn