Tristan Root's Blog

Wpf MVVM 3D Model Viewer – More View Panels

Posted by triroot on February 18, 2010

Introduction

In a previous post I showed you how to create a basic 3D model viewer in WPF using the MVVM pattern. In this post I’m going to show you how to take advantage of the MVVM design of this application by adding a couple more views (one view to move the camera, and one to change the mesh being viewed) and there will be one more view-model (for toggling the viewed mesh).

The Camera  View 

Here is the xaml for the Camera Control Panel, this will use the CameraVM as its DataContext.

<UserControl x:Class="TriRoot.Views.CameraPanel"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

   

    <StackPanel>

        <DockPanel>

            <TextBlock Text="Rotation:" Width="65" />

            <TextBlock DockPanel.Dock="Right" Width="65"

                Text="{Binding Rotation}" />

            <Slider x:Name="RotationSlider"

                Minimum="0" Maximum="6.283"

                Value="{Binding Rotation}"/>

        </DockPanel>

        <DockPanel >

            <TextBlock Text="Up / Down:" Width="65" />

            <TextBlock DockPanel.Dock="Right" Width="65"

                Text="{Binding UpPercent}" />

            <Slider x:Name="UpPercentSlider"

                Minimum="-1" Maximum="1"

                Value="{Binding UpPercent}"/>

        </DockPanel>

    </StackPanel>

   

</UserControl>

There is no additional code behind for this panel or the CameraVM. The sliders will update the values in the CameraVM through bindings when the user interacts with the sliders.

The following is an update to the test window to use this new panel. I’ve grayed out any code that was from the first post.

<Window x:Class="TriRoot.Test.TestWindow"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:views="clr-namespace:TriRoot.Views;assembly=TriRoot.Blog"

    xmlns:viewmodels="clr-namespace:TriRoot.ViewModels;assembly=TriRoot.Blog"

    Title="TriRoot.Test" Height="300" Width="300">

   

    <Window.Resources>

        <DataTemplate DataType="{x:Type viewmodels:ViewportVM}">

            <views:ViewportPanel />

        </DataTemplate>

        <DataTemplate DataType="{x:Type viewmodels:CameraVM}">

            <views:CameraPanel />

        </DataTemplate>

    </Window.Resources>

 

    <DockPanel>

        <ContentPresenter x:Name="CameraVMContentPresenter" DockPanel.Dock="Top"/>

        <ContentPresenter x:Name="ViewportVMContentPresenter" />       

    </DockPanel>

 

</Window>

 

And the CameraVM needs to be set as the Content of the new content presenter in the code behind 

CameraVM cameraVM = new CameraVM() { CameraData = cameraData };

MeshVM meshVM = new MeshVM() { MeshData = meshData };

ViewportVM viewportVM = new ViewportVM()

{

    Camera = cameraVM,

    Mesh = meshVM

};

 

CameraVMContentPresenter.Content = cameraVM;

ViewportVMContentPresenter.Content = viewportVM;

Here’s a screenshot of the result:

In another post I have the details on how to implement a simple Cube Mesh against the IMeshData interface. The resulting class is the MeshDataCube. Next I’m going to write a view model and view that allows the user to select between viewing a simple triangle or the MeshDataCube. 

The Mesh Editor View-Model

The mesh editor will have a couple commands for toggling the mesh in use. These commands get bound to buttons, so that when a user clicks the button, ICommand.Execute gets called. The DelegateCommand is an implementation of ICommand that takes an Action that gets invoked in DelegateCommand.Execute, and a Func<bool> that gets invoked and returned in DelegateCommand.CanExecute.  In this implementation, I’m ignoring the parameters of Execute and CanExecute.

public class DelegateCommand : ICommand

{

    Action _executeAction;

    Func<bool> _canExecuteFunc;

 

    public DelegateCommand(Action executeAction)

        : this(executeAction, null)

    {

    }

 

    public DelegateCommand(Action executeAction, Func<bool> canExecuteFunc)

    {

        _executeAction = executeAction;

        _canExecuteFunc = canExecuteFunc;

    }

 

    public event EventHandler CanExecuteChanged;

 

    public void Execute(object unused)

    {

        _executeAction();

    }

 

    public bool CanExecute(object unused)

    {

        return _canExecuteFunc != null ? _canExecuteFunc() : true;

    }

 

    public void FireCanExecuteChanged()

    {

        if (CanExecuteChanged != null)

        {

            CanExecuteChanged(this, new EventArgs());

        }

    }

}

The MeshEditor has two commands, one for switching to a triangle mesh, and one for switching to a cube mesh. The SelectedMesh property is a reference to the MeshVM which contains the MeshData that will be altered when a command executes.

public class MeshEditorVM : DependencyObject

{

    DelegateCommand _triangleCommand;

    DelegateCommand _cubeCommand;

 

    public MeshEditorVM()

    {

        _triangleCommand = new DelegateCommand(

            MakeTriangle,

            CanMakeTriangle);

 

        _cubeCommand = new DelegateCommand(

            MakeCube,

            CanMakeCube);

    }

 

    #region MeshVM SelectedMesh (DependencyProperty w/OnChanged)

    public static readonly DependencyProperty SelectedMeshDataProperty =

        DependencyProperty.Register(

            "SelectedMesh",

            typeof(MeshVM),

            typeof(MeshEditorVM),

            new FrameworkPropertyMetadata(OnSelectedMeshChanged));

 

    public MeshVM SelectedMesh

    {

        get { return (MeshVM)GetValue(SelectedMeshDataProperty); }

        set { SetValue(SelectedMeshDataProperty, value); }

    }

 

    protected static void OnSelectedMeshChanged(

        DependencyObject sender,

        DependencyPropertyChangedEventArgs args)

    {

        MeshEditorVM me = (MeshEditorVM)sender;

        me.RefreshCommands();

    }

    #endregion

 

    public ICommand TriangleCommand { get { return _triangleCommand; } }

    public ICommand CubeCommand { get { return _cubeCommand; } }

 

    private void RefreshCommands()

    {

        _triangleCommand.FireCanExecuteChanged();

        _cubeCommand.FireCanExecuteChanged();

    }

 

    private void MakeTriangle()

    {

        if (SelectedMesh != null)

        {

            SelectedMesh.MeshData = new MeshDataTriangle(

                new Point3D(0.5, 0, 0),

                new Point3D(0, 0.5, 0),

                new Point3D(0, 0, 0.5));

 

            RefreshCommands();

        }

    }

 

    private bool CanMakeTriangle()

    {

        return SelectedMesh != null

            && !(SelectedMesh.MeshData is MeshDataTriangle);

    }

 

    private void MakeCube()

    {

        if (SelectedMesh != null)

        {

            SelectedMesh.MeshData = new MeshDataCube(1.0);

            RefreshCommands();

        }

    }

 

    private bool CanMakeCube()

    {

        return SelectedMesh != null

            && !(SelectedMesh.MeshData is MeshDataCube);

    }

}

Here is the xaml for the panel that will use the MeshEditorVM as it s DataContext.

<UserControl x:Class="TriRoot.Views.MeshEditorPanel"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

   

    <StackPanel Background="LemonChiffon" Margin="2">

        <Button Content="Triangle" Command="{Binding TriangleCommand}" Margin="2"/>

        <Button Content="Cube" Command="{Binding CubeCommand}" Margin="2"/>

   </StackPanel>

   

</UserControl>

Again, the following is an update to the test window to use this new panel.

<Window x:Class="TriRoot.Test.TestWindow"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:views="clr-namespace:TriRoot.Views;assembly=TriRoot.Blog"

    xmlns:viewmodels="clr-namespace:TriRoot.ViewModels;assembly=TriRoot.Blog"

    Title="TriRoot.Test" Height="300" Width="300">

   

    <Window.Resources>

        <DataTemplate DataType="{x:Type viewmodels:ViewportVM}">

            <views:ViewportPanel />

        </DataTemplate>

        <DataTemplate DataType="{x:Type viewmodels:CameraVM}">

            <views:CameraPanel />

        </DataTemplate>

        <DataTemplate DataType="{x:Type viewmodels:MeshEditorVM}">

            <views:MeshEditorPanel />

        </DataTemplate>

    </Window.Resources>

 

    <DockPanel>

        <ContentPresenter x:Name="MeshEditorVMContentPresenter" DockPanel.Dock="Right"/>

        <ContentPresenter x:Name="CameraVMContentPresenter" DockPanel.Dock="Top"/>

        <ContentPresenter x:Name="ViewportVMContentPresenter" />       

    </DockPanel>

 

</Window>

And then in the code behind a MeshEditorVM gets hooked up to the new ContentPresenter.

ICameraData cameraData = new CameraData();

IMeshData meshData = new MeshDataTriangle(

    new Point3D(0.5, 0, 0),

    new Point3D(0, 0.5, 0),

    new Point3D(0, 0, 0.5));

 

CameraVM cameraVM = new CameraVM() { CameraData = cameraData };

MeshVM meshVM = new MeshVM() { MeshData = meshData };

ViewportVM viewportVM = new ViewportVM()

{

    Camera = cameraVM,

    Mesh = meshVM

};

 

MeshEditorVM meshEditorVM = new MeshEditorVM()

{

    SelectedMesh = meshVM

};

 

MeshEditorVMContentPresenter.Content = meshEditorVM;

CameraVMContentPresenter.Content = cameraVM;

ViewportVMContentPresenter.Content = viewportVM;

 

And in case you missed it from my first post, here’s the final screenshot:

BAM!

Posted in WPF | Tagged: , , , | Leave a Comment »

Wpf MVVM 3D Model Viewer – IMeshData Cube

Posted by triroot on February 16, 2010

In my last post I implemented a 3D Model Viewer that renders geometry from IMeshData. In the post I just rendered a simple triangle mesh. Here I’m going to implement a simple cube mesh. This implementation is designed to be easy to follow, and has not been optimized for saving memory.

For reference, here is what the IMeshData looks like:

public interface IMeshData

{

    IEnumerable<Point3D> Vertices { get; }

    IEnumerable<int> TriangleIndices { get; }

}

I implemented a static Utility class that will describe the data needed for each cube face.  The Contributions are the four corner points in 3D space for a face, and the TriangleIndices describes how to use those points to construct two triangles for the face

public enum CubeFaceType

{

    None,

    Left,

    Right,

    Top,

    Bottom,

    Front,

    Back,

}

class CubeFaceUtil

{

    internal static CubeFaceType[] GetCubeFaces()

    {

        return new CubeFaceType[]

        {

            CubeFaceType.Left,

            CubeFaceType.Right,

            CubeFaceType.Top,

            CubeFaceType.Bottom,

            CubeFaceType.Front,

            CubeFaceType.Back

        };

    }

 

    internal static Point3D[] GetContributions(CubeFaceType type)

    {

        switch (type)

        {

            case CubeFaceType.Left:

                return new Point3D[]

                {

                    new Point3D(-1, 1, -1),

                    new Point3D(-1, 1, 1),

                    new Point3D(-1, -1, 1),

                    new Point3D(-1, -1, -1)

                };

            case CubeFaceType.Right:

                return new Point3D[]

                {

                    new Point3D(1, 1, 1),

                    new Point3D(1, 1, -1),

                    new Point3D(1, -1, -1),

                    new Point3D(1, -1, 1)

                };

            case CubeFaceType.Top:

                return new Point3D[]

                {

                    new Point3D(-1, 1, -1),

                    new Point3D(1, 1, -1),

                    new Point3D(1, 1, 1),

                    new Point3D(-1, 1, 1)

                };

            case CubeFaceType.Bottom:

                return new Point3D[]

                {

                    new Point3D(1, -1, -1),

                    new Point3D(-1, -1, -1),

                    new Point3D(-1, -1, 1),

                    new Point3D(1, -1, 1)

                };

            case CubeFaceType.Front:

                return new Point3D[]

                {

                    new Point3D(-1, 1, 1),

                    new Point3D(1, 1, 1),

                    new Point3D(1, -1, 1),

                    new Point3D(-1, -1, 1)

                };

            case CubeFaceType.Back:

                return new Point3D[]

                {

                    new Point3D(1, 1, -1),

                    new Point3D(-1, 1, -1),

                    new Point3D(-1, -1, -1),

                    new Point3D(1, -1, -1)

                };

            default:

                throw new ArgumentException(

                    "Unsupported CubeFaceType",

                    "type");

        }

    }

 

    internal static int[] GetTriangleIndices()

    {

        return new int[]

            {

                0, 3, 1,

                1, 3, 2

            };

    }

}

Now all that needs to be done is to iterate through the faces and add them to the MeshDataCube.

public class MeshDataCube : IMeshData

{

    public MeshDataCube(double cubeSize)

    {

        double halfFaceSize = cubeSize / 2;

 

        List<int> allIndices = new List<int>();

        List<Point3D> allVertices = new List<Point3D>();

 

        foreach (CubeFaceType faceType in CubeFaceUtil.GetCubeFaces())

        {

            int[] indices = CubeFaceUtil.GetTriangleIndices();

            Point3D[] contributions = CubeFaceUtil.GetContributions(faceType);

            Point3D[] positions = contributions.Select(p => p.Scale(halfFaceSize)).ToArray();

 

            // shift the indicies by the number of vertices already added.

            allIndices.AddRange(indices.Select(i => i + allVertices.Count()));

            allVertices.AddRange(positions);

        }

 

        Vertices = allVertices;

        TriangleIndices = allIndices;

    }

 

    public IEnumerable<int> TriangleIndices { get; protected set; }

    public IEnumerable<Point3D> Vertices { get; protected set; }

}

BAM!

 

Posted in WPF | Tagged: , , | 1 Comment »

Wpf MVVM 3D Model Viewer

Posted by triroot on December 2, 2009

Introduction

In this post I’m going to walk you through a few of the basics for setting up a 3D model viewer using the MVVM (Model View View-Model) pattern in a WPF (Windows Presentation Foundation) application. I’ll show you how to set up a view port, camera control and a simple mesh.

This post assumes you have a basic understanding of WPF, MVVM, and 3D modeling.

Background

There are a couple of WPF classes that it would be worth quickly going to MSDN and browsing over their examples.

Viewport3D – provides a rendering surface for 3D content.

Camera – an abstract class that represents the viewing position and direction in 3D space. It describes how a 3D model is projected onto a 2D visual.

Visual3D – an abstract class that provides services and properties common to visual 3D objects.

Classes and Interfaces

The interfaces and classes that I’ll be implementing are the following:

IMeshData – a simple interface that represents a mesh’s model

ICameraData – a simple interface that represents the camera’s model

MeshVM – a class that represents the mesh’s view-model

CameraVM – a class that represents the camera’s view-model

ViewportPanel – the WPF view for the camera and meshes.

I’ve chosen to use interfaces for my models because it gives the user the flexibility to implement their own data, or to wrap external classes, or both.

The Mesh Model

I’m keeping the model very simple, if you want get a bit fancier with this interface, you could add normals, texture mapping, material metadata, and so on. The class that implements the mesh model interface is just a simple triangle mesh.

public interface IMeshData

{

    IEnumerable<Point3D> Vertices { get; }

    IEnumerable<int> TriangleIndices { get; }

}

public class MeshDataTriangle : IMeshData

{

    List<Point3D> _vertices = new List<Point3D>();

    public MeshDataTriangle(Point3D one, Point3D two, Point3D three)

    {

        _vertices.Add(one);

        _vertices.Add(two);

        _vertices.Add(three);

    }

    public IEnumerable<Point3D> Vertices

    {

        get { return _vertices; }

    }

    public IEnumerable<int> TriangleIndices

    {

        get { return new int[] { 0, 1, 2 }; }

    }

}

The Camera Model

I’m using a pretty straight forward representation of a camera. In this case the Rotation represents the X and Z coordinates around a center point, and the UpPercent represents how high the camera is above (or below) the origin.

public interface ICameraData

{

    /// <summary> value between 0 (included) and 2*PI (excluded) </summary>

    double Rotation { get; }

    /// <summary> value between -1.0 (looking straight up) and 1.0 (looking straight down) </summary>

    double UpPercent { get; }

    bool SetRotation(double rotation);

    bool SetUpPercent(double upPercent);

}

public class CameraData : ICameraData

{

    public double Rotation { get; protected set; }

    public double UpPercent { get; protected set; }

    public bool SetRotation(double rotation)

    {

        Rotation = rotation;

        return true;

    }

    public bool SetUpPercent(double upPercent)

    {

        UpPercent = upPercent;

        return true;

    }

}

I decided to make the SetRotation and SetUpPercent methods have return type bool, because I can see a case where the camera is locked in place and not modifiable by the user. However, in my example here I’m not doing anything interesting like that.

The View-Models

Okay, now onto something a bit more interesting. The view-model is the glue between the model and the view. In the case of the MeshVM, the model data gets set as input, and then it is automatically converted to something that can be bound directly to the view. I’ve seen implementations of the View-Model that implement INotifyPropertyChanged, or have a ViewModelBase class, but I prefer just having the view model inherit from DependencyObject so that I can use the sweet sweet goodness of Dependency Properties.

With the MeshVM, the MeshData property is a reference to the model and the Geometry property is what the view can be bound to. When the MeshData is changed, the Geometry will be generated by a call to the RefreshGeometry method.

public class MeshVM : DependencyObject

{

    #region IMeshData MeshData (DependencyProperty w/OnMeshDataChanged)

    public static readonly DependencyProperty MeshDataProperty =

        DependencyProperty.Register(

            "MeshData",

            typeof(IMeshData),

            typeof(MeshVM),

            new FrameworkPropertyMetadata(OnMeshDataChanged));

    public IMeshData MeshData

    {

        get { return (IMeshData)GetValue(MeshDataProperty); }

        set { SetValue(MeshDataProperty, value); }

    }

    protected static void OnMeshDataChanged(

        DependencyObject sender,

        DependencyPropertyChangedEventArgs args)

    {

        MeshVM me = (MeshVM)sender;

        IMeshData newMeshData = (IMeshData)args.NewValue;

        me.RefreshGeometry(newMeshData);

    }

    #endregion

    #region MeshGeometry3D Geometry (DependencyProperty)

    public static readonly DependencyProperty GeometryProperty =

        DependencyProperty.Register(

            "Geometry",

            typeof(MeshGeometry3D),

            typeof(MeshVM));

    public MeshGeometry3D Geometry

    {

        get { return (MeshGeometry3D)GetValue(GeometryProperty); }

        protected set { SetValue(GeometryProperty, value); }

    }

    #endregion

    private void RefreshGeometry(IMeshData mesh)

    {

        MeshGeometry3D geometry = new MeshGeometry3D();

        geometry.Positions = mesh != null

            ? new Point3DCollection(mesh.Vertices)

            : new Point3DCollection();

        geometry.TriangleIndices = mesh != null

            ? new Int32Collection(mesh.TriangleIndices)

            : new Int32Collection();

        Geometry = geometry;

    }

}

Next is the CameraVM. This is a bit more complicated than the MeshVM, but the idea is the same. In this case there are three variables that get bound to the view – the camera Position, the UpDirection and the LookDirection. I’ve also provided two additional variables that can be used to modify the camera’s data – the Rotation and the UpPercent. Notice that when the CameraData is set, the _isSettingData is set to true, that way the OnChanged events for the Rotation and UpPercent get skipped and RefreshCamera only gets called once.

For the sake of space, I’m going to collapse the dependency property regions because it’s mostly just boiler plate code anyway. I’ll leave the changed events since that’s where all the interesting stuff happens.

public class CameraVM : DependencyObject

{

    bool _isSettingData;

    ICameraData CameraData (DependencyProperty w/OnCameraDataChanged)

    protected static void OnCameraDataChanged(

        DependencyObject sender,

        DependencyPropertyChangedEventArgs args)

    {

        CameraVM me = (CameraVM)sender;

        ICameraData newCameraData = (ICameraData)args.NewValue;

        me._isSettingData = true;

        me.Rotation = newCameraData.Rotation;

        me.UpPercent = newCameraData.UpPercent;

        me._isSettingData = false;

        me.RefreshCamera(newCameraData);

    }

    double Rotation (DependencyProperty w/OnRotationChanged)

    protected static void OnRotationChanged(

        DependencyObject sender,

        DependencyPropertyChangedEventArgs args)

    {

        CameraVM me = (CameraVM)sender;

        if (!me._isSettingData)

        {

            double newRotation = (double)args.NewValue;

            if (me.CameraData != null

                && me.CameraData.SetRotation(newRotation))

            {

                me.RefreshCamera(me.CameraData);

            }

        }

    }

    double UpPercent (DependencyProperty w/OnUpPercentChanged)

    protected static void OnUpPercentChanged(

        DependencyObject sender,

        DependencyPropertyChangedEventArgs args)

    {

        CameraVM me = (CameraVM)sender;

        if (!me._isSettingData)

        {

            double newUpPercent = (double)args.NewValue;

            if (me.CameraData != null

                && me.CameraData.SetUpPercent(newUpPercent))

            {

                me.RefreshCamera(me.CameraData);

            }

        }

    }

    Point3D Position (DependencyProperty)

    Vector3D LookDirection (DependencyProperty)

    Vector3D UpDirection (DependencyProperty)

    private void RefreshCamera(ICameraData cameraData)

    {

        double rotation = cameraData != null

            ? cameraData.Rotation

            : 0;

        double cameraX = Math.Sin(rotation);

        double cameraZ = Math.Cos(rotation);

        double upDownPercent = cameraData != null

            ? cameraData.UpPercent

            : 0;

        double rotationPercent = 1.0 – Math.Abs(upDownPercent);

        Vector3D straightUpVector = new Vector3D(0.0, 1.0, 0.0);

        Vector3D rotationVector = new Vector3D(cameraX, 0.0, cameraZ);

        Vector3D rightVector = Vector3D.CrossProduct(

            straightUpVector,

            rotationVector);

        Vector3D positionVector = new Vector3D(

            rotationVector.X * rotationPercent,

            straightUpVector.Y * upDownPercent,

            rotationVector.Z * rotationPercent);

        Vector3D lookVector = Vector3D.Subtract(

            new Vector3D(),

            positionVector);

        Vector3D upVector = Vector3D.CrossProduct(

            positionVector,

            rightVector);

        upVector.Normalize();

        lookVector.Normalize();

        positionVector.Normalize();

        positionVector = positionVector * 2.5;

        Position = new Point3D(

            positionVector.X,

            positionVector.Y,

            positionVector.Z);

        LookDirection = lookVector;

        UpDirection = upVector;

    }

}

The camera will always be looking at the origin, so in figuring out the Position of the camera, the LookDirection can easily be calculated by subtracting the Position vector from the zero vector. The UpDirection can be calculated by keeping track of the right vector and then crossing it with the final Position vector.


There needs to be a view model that ties all the other view models together.  So here is the ViewportVM, it has two dependency properties, one for the MeshVM and one for the CameraVM.

public class ViewportVM : DependencyObject

{

    CameraVM Camera (DependencyProperty)

    MeshVM Mesh (DependencyProperty)

}

The View

Now we need a view that all this data can hooked up to. The ViewportPanel is a simple UserControl that will contain the 3D view port, the camera, and one mesh. I’m also going to cheat a little and just throw in a couple lights to beautify the scene. The code behind is literally just what you get when you use the VisualStudio UserControl template.

public partial class ViewportPanel : UserControl

{

    public ViewportPanel()

    {

        InitializeComponent();

    }

}

The ViewportPanel will use the ViewportVM as the DataContext. So in the xaml, the Mesh and Camera get bound to the MeshVM and CameraVM respectively.

<UserControl x:Class="TriRoot.Views.ViewportPanel"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Viewport3D>

        <Viewport3D.Camera>

            <PerspectiveCamera x:Name="camera"

                Position="{Binding Camera.Position}"

                LookDirection="{Binding Camera.LookDirection}"

                UpDirection="{Binding Camera.UpDirection}"/>

        </Viewport3D.Camera>

        <ModelUIElement3D x:Name="model">

            <ModelUIElement3D.Model>

                <GeometryModel3D Geometry="{Binding Mesh.Geometry}">

                    <GeometryModel3D.Material>

                        <DiffuseMaterial Brush="LightGray" />

                    </GeometryModel3D.Material>

                </GeometryModel3D>

            </ModelUIElement3D.Model>

        </ModelUIElement3D>

        <ModelVisual3D>

            <ModelVisual3D.Content>

                <PointLight Color="White" Position="9, 12, 10" />

            </ModelVisual3D.Content>

        </ModelVisual3D>

        <ModelVisual3D>

            <ModelVisual3D.Content>

                <PointLight Color="White" Position="-10, -12, -8" />

            </ModelVisual3D.Content>

        </ModelVisual3D>

    </Viewport3D>

</UserControl>

Almost done… The last and final step is to write a test window where an instance of each of these objects is created.  I’ve chosen to use a ContentPresenter so that when I add a view model as its content, the view model is automatically set as the content’s DataContext. This way a designer can go in and modify the ViewportVM DataTemplate without having to worry about what’s happening in the code behind. Here it is:

<Window x:Class="TriRoot.Test.TestWindow"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:views="clr-namespace:TriRoot.Views;assembly=TriRoot.Blog"

    xmlns:viewmodels="clr-namespace:TriRoot.ViewModels;assembly=TriRoot.Blog"

    Title="TriRoot.Test" Height="300" Width="300">

    <Window.Resources>

        <DataTemplate DataType="{x:Type viewmodels:ViewportVM}">

            <views:ViewportPanel />

        </DataTemplate>

    </Window.Resources>

    <ContentPresenter x:Name="ViewportVMContentPresenter" />       

</Window>

 And the code behind:

public partial class TestWindow : System.Windows.Window

{

    public TestWindow()

    {

        InitializeComponent();

        ICameraData cameraData = new CameraData();

        IMeshData meshData = new MeshDataTriangle(

            new Point3D(0.5, 0, 0),

            new Point3D(0, 0.5, 0),

            new Point3D(0, 0, 0.5));

        CameraVM cameraVM = new CameraVM() { CameraData = cameraData };

        MeshVM meshVM = new MeshVM() { MeshData = meshData };

        ViewportVM viewportVM = new ViewportVM()

            {

                Camera = cameraVM,

                Mesh = meshVM

            };

        ViewportVMContentPresenter.Content = viewportVM;

    }

}

Here’s a sample screenshot of the result:

Well, that’s it for now. The basic framework is in place to fully take advantage of a MVVM design, and in my next post I’ll do just that. I’ll walk you through the implementation of the top camera control panel and the side mesh selector panel that I teased you with in the introduction of this post.

BAM!

Posted in WPF | Tagged: , , , | 2 Comments »

 
Follow

Get every new post delivered to your Inbox.