Click here to Skip to main content
15,895,656 members
Articles / Desktop Programming / WPF

Building an Extensible Application with MEF, WPF, and MVVM

Rate me:
Please Sign up or sign in to vote.
4.88/5 (45 votes)
15 Nov 2009LGPL316 min read 303.3K   7.4K   185  
An article for anyone interested in how to build an extensible application using WPF and the Model-View-ViewModel pattern.
#region "SoapBox.Core License"
/// <header module="SoapBox.Core"> 
/// Copyright (C) 2009 SoapBox Automation Inc., All Rights Reserved.
/// Contact: SoapBox Automation Licencing (license@soapboxautomation.com)
/// 
/// This file is part of SoapBox Core.
/// 
/// Commercial Usage
/// Licensees holding valid SoapBox Automation Commercial licenses may use  
/// this file in accordance with the SoapBox Automation Commercial License
/// Agreement provided with the Software or, alternatively, in accordance 
/// with the terms contained in a written agreement between you and
/// SoapBox Automation Inc.
/// 
/// GNU Lesser General Public License Usage
/// SoapBox Core is free software: you can redistribute it and/or modify 
/// it under the terms of the GNU Lesser General Public License
/// as published by the Free Software Foundation, either version 3 of the
/// License, or (at your option) any later version.
/// 
/// SoapBox Core is distributed in the hope that it will be useful, 
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
/// GNU Lesser General Public License for more details.
/// 
/// You should have received a copy of the GNU Lesser General Public License 
/// along with SoapBox Core. If not, see <http://www.gnu.org/licenses/>.
/// </header>
#endregion

using System;
using System.Collections.Generic;
using System.Linq;

using AdvanceMath;
using Physics2DDotNet;
using Physics2DDotNet.Detectors;
using Physics2DDotNet.Solvers;
using Physics2DDotNet.PhysicsLogics;
using Physics2DDotNet.Shapes;
using SoapBox.Core;
using System.ComponentModel.Composition;
using System.Collections.ObjectModel;
using System.Windows.Media;
using System.Windows;
using Physics2DDotNet.Joints;
using System.Windows.Threading;
using System.Threading;
using System.Windows.Input;
using System.ComponentModel;

namespace SoapBox.Core.Arena
{
    public abstract class AbstractArena : AbstractDocument, IArena
    {
        #region "Gravity"
        /// <summary>
        /// Represents the gravity field in the Arena.
        /// Defaults to no gravity.
        /// Can include left/right(X) and up/down(Y) components.
        /// Positive X is to the right, Positive Y is up.
        /// </summary>
        public ArenaVector Gravity
        {
            get
            {
                return m_Gravity;
            }
            set
            {
                if (m_Gravity.X != value.X || m_Gravity.Y != value.Y)
                {
                    m_Gravity = value;
                    m_GravityLogic.Lifetime.IsExpired = true;
                    m_GravityLogic = (PhysicsLogic)new GravityField(new Vector2D(m_Gravity.X, m_Gravity.Y), new Lifespan());
                    m_engine.AddLogic(m_GravityLogic);
                    NotifyPropertyChanged(m_GravityArgs);
                }
            }
        }
        private ArenaVector m_Gravity = new ArenaVector(); // default zero
        static readonly PropertyChangedEventArgs m_GravityArgs =
            NotifyPropertyChangedHelper.CreateArgs<AbstractArena>(o => o.Gravity);
        private PhysicsLogic m_GravityLogic = (PhysicsLogic)new GravityField(new Vector2D(0f, 0f), new Lifespan());
        #endregion

        #region "Scale"
        /// <summary>
        /// Represents the number of screen elements (pixels?)
        /// per each physics unit (meters?).
        /// </summary>
        public float Scale
        {
            get
            {
                return m_Scale;
            }
            set
            {
                if (m_Scale != value)
                {
                    m_Scale = value;
                    NotifyPropertyChanged(m_ScaleArgs);
                }
            }
        }
        private float m_Scale = 1.0f;
        static readonly PropertyChangedEventArgs m_ScaleArgs =
            NotifyPropertyChangedHelper.CreateArgs<AbstractArena>(o => o.Scale);
        #endregion

        #region "TargetInterval"
        /// <summary>
        /// Represents the period (seconds) between running
        /// the physics calculations.
        /// </summary>
        public float TargetInterval
        {
            get
            {
                return m_timer.TargetInterval;
            }
            set
            {
                if (m_timer.TargetInterval != value)
                {
                    m_timer.TargetInterval = value;
                    NotifyPropertyChanged(m_TargetIntervalArgs);
                }
            }
        }
        static readonly PropertyChangedEventArgs m_TargetIntervalArgs =
            NotifyPropertyChangedHelper.CreateArgs<AbstractArena>(o => o.TargetInterval);
        #endregion

        #region "Bodies"
        /// <summary>
        /// A collection of bodies in the arena.
        /// Use AddArenaBody and RemoveArenaBody
        /// to manipulate this collection from the
        /// derived class.
        /// </summary>
        public IEnumerable<IArenaBody> Bodies
        {
            get
            {
                return from obj in m_Bodies orderby obj.Sprite.ZIndex select obj;
            }
        }
        private readonly ObservableCollection<IArenaBody> m_Bodies = 
            new ObservableCollection<IArenaBody>();
        
        // Each item will have a "body" that represents it in the
        // physics engine, so keep track of that mapping
        private readonly Dictionary<IArenaBody, Body> m_BodiesLookup =
            new Dictionary<IArenaBody, Body>();
        private readonly Dictionary<Body, IArenaBody> m_BodiesReverseLookup =
            new Dictionary<Body, IArenaBody>();


        /// <summary>
        /// Add a body to the arena.  
        /// Use AbstractArenaFreeBody or AbstractArenaStationaryBody 
        /// as a base.
        /// </summary>
        /// <param name="item"></param>
        protected void AddArenaBody(IArenaBody obj)
        {
            AddArenaBody(obj, 2f);
        }

        /// <summary>
        /// Default grid spacing is 2.  Override it with a higher number
        /// for less accuracy, but better performance.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="gridSpacing"></param>
        protected void AddArenaBody(IArenaBody obj, float gridSpacing)
        {
            if (!m_Bodies.Contains(obj))
            {
                m_Bodies.Add(obj);

                // Copy the initial properties to the state
                obj.State.Position.X = obj.InitialX;
                obj.State.Position.Y = obj.InitialY;
                obj.State.Angle = obj.InitialAngle;

                // Add it to the physics engine
                PhysicsState state = new PhysicsState(new ALVector2D(obj.InitialAngle, obj.InitialX, obj.InitialY));
                IShape shape = null;
                Coefficients coff = new Coefficients(obj.Restitution, obj.Friction, obj.Friction);
                Lifespan life = new Lifespan();

                // Special case - circles.  They won't roll right unless we define them as CircleShapes.
                if (obj.Sprite.Geometry is EllipseGeometry)
                {
                    EllipseGeometry eg = (EllipseGeometry)obj.Sprite.Geometry;
                    if (eg.RadiusX == eg.RadiusY)
                    {
                        shape = new CircleShape((float)eg.RadiusX, Convert.ToInt32(eg.RadiusX) * 4);
                    }
                }

                if (shape == null)
                {
                    // Have to convert the geometry into a polygon (points)
                    Collection<Point> points = new Collection<Point>();
                    PathGeometry pg = obj.Sprite.Geometry.GetFlattenedPathGeometry(1f, ToleranceType.Absolute);
                    foreach (PathFigure pf in pg.Figures)
                    {
                        // a figure is a list of segments, and a starting point
                        points.Add(pf.StartPoint);
                        foreach (PathSegment ps in pf.Segments)
                        {
                            // Path segments can be made up of LineSegments or
                            // PolyLineSegments
                            if (ps is LineSegment)
                            {
                                Point p = ((LineSegment)ps).Point;
                                if (!points.Contains(p))
                                {
                                    points.Add(p);
                                }
                            }
                            else if (ps is PolyLineSegment)
                            {
                                foreach (Point p in ((PolyLineSegment)ps).Points)
                                {
                                    if (!points.Contains(p))
                                    {
                                        points.Add(p);
                                    }
                                }
                            }
                            else
                            {
                                // ignore?
                            }
                        }
                    }

                    // convert to an array of Vector2D objects so Physics2D.Net understands it
                    Vector2D[] vectorPoints = new Vector2D[points.Count];
                    for (int i = 0; i < points.Count; i++)
                    {
                        vectorPoints[i] = new Vector2D(
                            Convert.ToSingle(points[i].X),
                            Convert.ToSingle(points[i].Y));
                    }

                    if (vectorPoints.Length < 2)
                    {
                        shape = new CircleShape(float.Epsilon, 4);
                    }
                    else
                    {
                        shape = new PolygonShape(VertexHelper.Subdivide(vectorPoints, 5), gridSpacing);
                    }
                }

                Body bdy;
                if (obj is IArenaPivotingBody)
                {
                    IArenaPivotingBody pivotObj = (IArenaPivotingBody)obj;

                    // Have to calculate the position of the joint, accounting for the initial angle
                    // theta1 is the initial angle of the pivot point relative to the center of mass
                    double theta1 = Math.Atan2(pivotObj.PivotPoint.Y, pivotObj.PivotPoint.X);
                    double newTheta = theta1 + pivotObj.InitialAngle;
                    double magnitude = Math.Sqrt(pivotObj.PivotPoint.X * pivotObj.PivotPoint.X + 
                        pivotObj.PivotPoint.Y * pivotObj.PivotPoint.Y);
                    float jointX = Convert.ToSingle(magnitude * Math.Cos(newTheta));
                    float jointY = Convert.ToSingle(magnitude * Math.Sin(newTheta));

                    bdy = new Body(state, shape, ((IArenaPivotingBody)obj).Mass, coff, life);
                    bdy.AngularDamping = 1.0f - ((IArenaPivotingBody)obj).PivotFriction;
                    bdy.IgnoresGravity = ((IArenaPivotingBody)obj).IgnoresGravity;
                    m_engine.AddBody(bdy);
                    FixedHingeJoint jnt = new FixedHingeJoint(bdy, 
                        new Vector2D(obj.InitialX + jointX, obj.InitialY + jointY), 
                        life);
                    jnt.Softness = 0;
                    jnt.DistanceTolerance = 200f;
                    m_engine.AddJoint(jnt);
                }
                else if (obj is IArenaDynamicBody)
                {
                    bdy = new Body(state, shape, ((IArenaDynamicBody)obj).Mass, coff, life);
                    bdy.IgnoresGravity = ((IArenaDynamicBody)obj).IgnoresGravity;
                    m_engine.AddBody(bdy);
                }
                else if (obj is IArenaDecorationBody)
                {
                    bdy = new Body(state, shape, float.PositiveInfinity, coff, life);
                    bdy.IgnoresGravity = true;
                    bdy.IgnoresCollisionResponse = true;
                    m_engine.AddBody(bdy);
                }
                else if (obj is IArenaStationaryBody)
                {
                    bdy = new Body(state, shape, float.PositiveInfinity, coff, life);
                    bdy.IgnoresGravity = true;
                    m_engine.AddBody(bdy);
                }
                else
                {
                    bdy = new Body(state, shape, 0f, coff, life);
                    m_engine.AddBody(bdy);
                }

                // hook up the collided event
                bdy.Collided += new EventHandler<CollisionEventArgs>(Body_Collided);
                
                // hook up the sprite ZIndex changed event handler
                obj.Sprite.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Sprite_PropertyChanged);

                m_BodiesLookup.Add(obj, bdy);
                m_BodiesReverseLookup.Add(bdy, obj);
            }
        }

        /// <summary>
        /// Remove a body from the arena.
        /// </summary>
        /// <param name="item"></param>
        protected void RemoveArenaBody(IArenaBody obj)
        {
            if (m_Bodies.Contains(obj))
            {
                m_Bodies.Remove(obj);

                // Remove it from the physics engine
                m_BodiesLookup[obj].Lifetime.IsExpired = true;
                m_BodiesReverseLookup.Remove(m_BodiesLookup[obj]);
                m_BodiesLookup.Remove(obj);
            }
        }
        #endregion
        #region "Body Collisions"
        private void Body_Collided(object sender, CollisionEventArgs e)
        {
            Body bdy = sender as Body;
            if (bdy != null)
            {
                Body otherBody = null;
                if (e.Contact.Body1 == bdy) // reference equality
                {
                    otherBody = e.Contact.Body2;
                }
                else
                {
                    otherBody = e.Contact.Body1;
                }
                if (otherBody != null)
                {
                    lock (m_bodyCollisionQueue_Lock)
                    {
                        m_bodyCollisionQueue.Enqueue(new CollisionBodies(bdy, otherBody));
                    }
                }
            }
        }

        private readonly Queue<CollisionBodies> m_bodyCollisionQueue = new Queue<CollisionBodies>();
        private object m_bodyCollisionQueue_Lock = new object();

        private class CollisionBodies
        {
            public Body Body1 { get; private set; }
            public Body Body2 { get; private set; }

            public CollisionBodies(Body body1, Body body2)
            {
                Body1 = body1;
                Body2 = body2;
            }
        }
        #endregion
        #region "Sprite PropertyChanged"

        void Sprite_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == m_ZIndexName)
            {
                NotifyPropertyChanged(m_BodiesArgs);
            }
        }
        static readonly PropertyChangedEventArgs m_BodiesArgs =
            NotifyPropertyChangedHelper.CreateArgs<AbstractArena>(o => o.Bodies);
        static readonly string m_ZIndexName =
            NotifyPropertyChangedHelper.GetPropertyName<ISprite>(o => o.ZIndex);
        #endregion

        #region "ViewPortWidth"
        /// <summary>
        /// Represents the number of screen elements (pixels?)
        /// per each physics unit (meters?).
        /// </summary>
        public float ViewPortWidth
        {
            get
            {
                return m_ViewPortWidth;
            }
            set
            {
                if (m_ViewPortWidth != value)
                {
                    if (value < 0f)
                    {
                        throw new ArgumentOutOfRangeException(m_ViewPortWidthName);
                    }
                    m_ViewPortWidth = value;
                    NotifyPropertyChanged(m_ViewPortWidthArgs);
                    NotifyPropertyChanged(m_ViewPortOffsetXArgs);
                }
            }
        }
        private float m_ViewPortWidth;
        static readonly PropertyChangedEventArgs m_ViewPortWidthArgs =
            NotifyPropertyChangedHelper.CreateArgs<AbstractArena>(o => o.ViewPortWidth);
        static readonly string m_ViewPortWidthName =
            NotifyPropertyChangedHelper.GetPropertyName<AbstractArena>(o => o.ViewPortWidth);
        #endregion

        #region "ViewPortHeight"
        /// <summary>
        /// Represents the number of screen elements (pixels?)
        /// per each physics unit (meters?).
        /// </summary>
        public float ViewPortHeight
        {
            get
            {
                return m_ViewPortHeight;
            }
            set
            {
                if (m_ViewPortHeight != value)
                {
                    if (value < 0f)
                    {
                        throw new ArgumentOutOfRangeException(m_ViewPortHeightName);
                    }
                    m_ViewPortHeight = value;
                    NotifyPropertyChanged(m_ViewPortHeightArgs);
                    NotifyPropertyChanged(m_ViewPortOffsetYArgs);
                }
            }
        }
        private float m_ViewPortHeight;
        static readonly PropertyChangedEventArgs m_ViewPortHeightArgs =
            NotifyPropertyChangedHelper.CreateArgs<AbstractArena>(o => o.ViewPortHeight);
        static readonly string m_ViewPortHeightName =
            NotifyPropertyChangedHelper.GetPropertyName<AbstractArena>(o => o.ViewPortHeight);
        #endregion

        #region "ViewPortOffsetX"
        /// <summary>
        /// Represents the number of screen elements (pixels?)
        /// per each physics unit (meters?).
        /// </summary>
        public float ViewPortOffsetX
        {
            get
            {
                return m_ViewPortWidth / 2;
            }
        }
        static readonly PropertyChangedEventArgs m_ViewPortOffsetXArgs =
            NotifyPropertyChangedHelper.CreateArgs<AbstractArena>(o => o.ViewPortOffsetX);
        #endregion

        #region "ViewPortOffsetY"
        /// <summary>
        /// Represents the number of screen elements (pixels?)
        /// per each physics unit (meters?).
        /// </summary>
        public float ViewPortOffsetY
        {
            get
            {
                return m_ViewPortHeight / 2;
            }
        }
        static readonly PropertyChangedEventArgs m_ViewPortOffsetYArgs =
            NotifyPropertyChangedHelper.CreateArgs<AbstractArena>(o => o.ViewPortOffsetY);
        #endregion

        Object m_engineLock = new object();
        PhysicsEngine m_engine;
        PhysicsTimer m_timer;

        public AbstractArena()
        {
            // Setup the Physics Engine
            m_engine = new PhysicsEngine();
            m_engine.BroadPhase = (BroadPhaseCollisionDetector)new SweepAndPruneDetector();
            m_engine.Solver = (CollisionSolver)new SequentialImpulsesSolver();

            // Pulled these values from their demo.  They are "popular" choices.
            SequentialImpulsesSolver phsSolver = (SequentialImpulsesSolver)m_engine.Solver;
            phsSolver.Iterations = 12;
            phsSolver.SplitImpulse = true;
            phsSolver.BiasFactor = 0.7f;
            phsSolver.AllowedPenetration = 0.1f;

            // Determines how fast we recalculate the simulation
            // Can be controlled with the TargetInterval property
            m_timer = new PhysicsTimer(timer_Callback, 0.01f);

            // Default gravity field is nothing
            m_engine.AddLogic(m_GravityLogic);

        }

        void timer_Callback(float dt, float trueDt)
        {
            // Be careful here, this runs on another thread
            lock (m_engineLock)
            {
                m_engine.Update(dt, trueDt);
            }

            // We're using this as a bit of a hack.  By telling the View that BodyCheck
            // has changed (which it binds to), it forces that thread
            // to call the Get method, which updates the bodies safely.
            NotifyPropertyChanged(m_BodyCheckArgs);
        }

        public bool BodyCheck
        {
            get
            {
                updateBodies();
                return true;
            }
        }
        static readonly PropertyChangedEventArgs m_BodyCheckArgs =
            NotifyPropertyChangedHelper.CreateArgs<AbstractArena>(o => o.BodyCheck);

        private void updateBodies()
        {
            lock (m_engineLock)
            {
                foreach (Body bdy in m_BodiesReverseLookup.Keys)
                {
                    IArenaBody obj = m_BodiesReverseLookup[bdy];

                    // Update physics quantities
                    obj.State.Position.X = bdy.State.Position.X;
                    obj.State.Position.Y = bdy.State.Position.Y;
                    obj.State.Angle = bdy.State.Position.Angular;
                    obj.State.Velocity.X = bdy.State.Velocity.X;
                    obj.State.Velocity.Y = bdy.State.Velocity.Y;

                    // Update screen co-ordinates
                    obj.State.ScreenX = (bdy.State.Position.X * Scale) + ViewPortOffsetX;
                    obj.State.ScreenY = -(bdy.State.Position.Y * Scale) + ViewPortOffsetY;
                    obj.State.ScreenAngle = -obj.State.Angle * 180.0f / Convert.ToSingle(Math.PI);
                    obj.State.Scale = Scale;

                    // allows each item to respond to the new state
                    obj.OnUpdate(); 

                    if (obj is IArenaDynamicBody)
                    {
                        IArenaDynamicBody arenaBody = (IArenaDynamicBody)obj;
                        bdy.ApplyTorque(arenaBody.Torque);
                        bdy.ApplyForce(
                            new Vector2D(arenaBody.Force.X, arenaBody.Force.Y),
                            new Vector2D(arenaBody.ForcePosition.X, arenaBody.ForcePosition.Y));
                        bdy.ApplyImpulse(
                            new Vector2D(arenaBody.Impulse.X, arenaBody.Impulse.Y),
                            new Vector2D(arenaBody.ImpulsePosition.X, arenaBody.ImpulsePosition.Y));
                    }
                }
            }
            lock (m_bodyCollisionQueue_Lock)
            {
                while (m_bodyCollisionQueue.Count > 0)
                {
                    CollisionBodies bodies = m_bodyCollisionQueue.Dequeue();
                    IArenaBody obj = m_BodiesReverseLookup[bodies.Body1];
                    IArenaBody otherObj = m_BodiesReverseLookup[bodies.Body2];
                    obj.OnCollision(otherObj);
                }
            }
        }

        #region "Control the Simulation"

        protected void Start()
        {
            m_timer.IsRunning = true;
            NotifyPropertyChanged(m_IsRunningArgs);
        }

        protected void Stop()
        {
            m_timer.IsRunning = false;
            NotifyPropertyChanged(m_IsRunningArgs);
        }

        /// <summary>
        /// Readonly property that returns true if the 
        /// simulation is running, false otherwise.
        /// Control this with the Start/Stop methods.
        /// </summary>
        public bool IsRunning
        {
            get
            {
                return m_timer.IsRunning;
            }
        }
        static readonly PropertyChangedEventArgs m_IsRunningArgs =
            NotifyPropertyChangedHelper.CreateArgs<AbstractArena>(o => o.IsRunning);

        #endregion

        #region "Keyboard Input"
        public virtual void OnKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { }
        public virtual void OnKeyUp(object sender, System.Windows.Input.KeyEventArgs e) { }
        #endregion

        #region "Mouse Input"
        public virtual void OnMouseDown(object sender, MouseButtonEventArgs e) { }
        public virtual void OnMouseUp(object sender, MouseButtonEventArgs e) { }
        public virtual void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { }
        public virtual void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { }
        public virtual void OnMouseRightButtonDown(object sender, MouseButtonEventArgs e) { }
        public virtual void OnMouseRightButtonUp(object sender, MouseButtonEventArgs e) { }
        public virtual void OnMouseMove(object sender, MouseEventArgs e) { }
        public virtual void OnMouseWheel(object sender, MouseEventArgs e) { }
        public virtual void OnMouseEnter(object sender, MouseEventArgs e) { }
        public virtual void OnMouseLeave(object sender, MouseEventArgs e) { }
        #endregion

        #region "Focus"
        // The layout manager can give us focus (by the user clicking on the document tab)
        // without us ever getting focus directly.  Therefore we have to handle the 
        // OnGotFocus method and raise our own event, and let our View make sure it grabs
        // the focus appropriately.  This is only because the Arena is a weird beast.  It
        // has a bunch of visual controls (shapes, etc.) that don't normally get keyboard
        // focus, and yet we want to react to keyboard input.
        public event RoutedEventHandler GotFocus;
        public override void OnGotFocus(object sender, RoutedEventArgs e)
        {
            base.OnGotFocus(sender, e);
            GotFocus(this, e);
        }
        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Engineer
Canada Canada
By day I'm a Professional Engineer, doing .NET, VB6, SQL Server, and Automation (Ladder Logic, etc.) programming.

On weekends I write and maintain an open source extensible application framework called SoapBox Core.

In the evenings I provide front line technical support for moms4mom.com and I help out with administrative tasks (like formatting stuff). I also pitch in as a moderator from time to time.

You can follow me on twitter.

Comments and Discussions