Subclassing UIElement3D
Subclassing from UIElement3D to create your own elements that respond to input, focus and eventing is simple to do in 3.5. In this example we'll create a Sphere class which derives from UIElement3D and will show off some new features in the process.
Deriving from UIElement3D
The first step is to derive from UIElement3D:
public class Sphere : UIElement3D
{
}
Even though UIElement3D is declared abstract, there aren't any abstract methods you need to override. The above will compile - it just won't do anything interesting.
Rendering a Sphere
Although the above lets us derive from UIElement3D, we still don't have anything rendering to look like a sphere. Before going in to how to render the sphere, first we're going to want to create a couple of dependency properties to control how the sphere looks. These will be PhiDiv and ThetaDiv, to represent the number of slices to make in the horizontal and vertical directions respectively, as well as Radius, to represent the radius of the sphere. The code for these is below:
/// <summary>
/// The number of vertical slices to make on the sphere
/// </summary>
public static readonly DependencyProperty ThetaDivProperty =
DependencyProperty.Register("ThetaDiv",
typeof(int),
typeof(Sphere),
new PropertyMetadata(15, ThetaDivPropertyChanged));
private static void ThetaDivPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Sphere s = (Sphere)d;
s.InvalidateModel();
}
public int ThetaDiv
{
get
{
return (int)GetValue(ThetaDivProperty);
}
set
{
SetValue(ThetaDivProperty, value);
}
}
/// <summary>
/// The number of horizontal slices to make on the sphere
/// </summary>
public static readonly DependencyProperty PhiDivProperty =
DependencyProperty.Register("PhiDiv",
typeof(int),
typeof(Sphere),
new PropertyMetadata(15, PhiDivPropertyChanged));
private static void PhiDivPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Sphere s = (Sphere)d;
s.InvalidateModel();
}
public int PhiDiv
{
get
{
return (int)GetValue(PhiDivProperty);
}
set
{
SetValue(PhiDivProperty, value);
}
}
/// <summary>
/// The radius of the sphere
/// </summary>
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register("Radius",
typeof(double),
typeof(Sphere),
new PropertyMetadata(1.0, RadiusPropertyChanged));
private static void RadiusPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Sphere s = (Sphere)d;
s.InvalidateModel();
}
public double Radius
{
get
{
return (double)GetValue(RadiusProperty);
}
set
{
SetValue(RadiusProperty, value);
}
}
InvalidateModel, OnUpdateModel and Visual3DModel
The above should look like standard C# WPF code to create a few dependency properties, but the one new method that should look unfamiliar is the InvalidateModel call made whenever any of the above dependency properties changes. InvalidateModel is similar to the InvalidateVisual method that exists for the 2D UIElement. Calling InvalidateModel indicates that the 3D model representing the UIElement3D has changed and should be updated.
In response to InvalidateModel, OnUpdateModel will be called to update the 3D model that represents the object. In this way, you can make multiple changes to properties that affect the visual appearance of the UIElement3D and only make one final change to the model, rather than having to regenerate it each time a change is made. For instance, say changes are made to ThetaDiv, PhiDiv, and Radius above. Rather than having to regenerate the model each time, InvalidateModel can be called each time a property changes, and then all of the changes can be dealt with when OnUpdateModel is called. Of course, the model can also be changed in other places, but this provides one standard option to make the change.
The last thing that hasn't been discussed is how to actually change the model. In 3.5, Visual3D now exposes a protected CLR property Visual3DModel which represents the 3D model for the object. This is the 3D equivalent to the render data of a 2D Visual. To set what the visual representation for the Visual3D is then, it's necessary to set this property. For instance, ModelUIElement3D's Model property takes care of setting this. There is one subtle point about setting this property. Just like render data, setting this property won't set up the links necessary for things such as data bindings to work. To make this happen, you'll also want to have a dependency property for the model itself.
The code for InvalidateModel is shown below, as well as the Model dependency property:
protected override void OnUpdateModel()
{
GeometryModel3D model = new GeometryModel3D();
model.Geometry = Tessellate(ThetaDiv, PhiDiv, Radius);
model.Material = new DiffuseMaterial(Brushes.Blue);
Model = model;
}
/// <summary>
/// The Model property for the sphere
/// </summary>
private static readonly DependencyProperty ModelProperty =
DependencyProperty.Register("Model",
typeof(Model3D),
typeof(Sphere),
new PropertyMetadata(ModelPropertyChanged));
private static void ModelPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Sphere s = (Sphere)d;
s.Visual3DModel = (Model3D)e.NewValue;
}
private Model3D Model
{
get
{
return (Model3D)GetValue(ModelProperty);
}
set
{
SetValue(ModelProperty, value);
}
}
Attached to this post you'll find a simple example app which has the full code to the above Sphere class.
Comments
Anonymous
September 05, 2007
One of the great new additions in 3.5 is UIElement3D, which brings input, focus, and eventing to theAnonymous
September 05, 2007
One of the great new additions in 3.5 is UIElement3D, which brings input, focus, and eventing to theAnonymous
September 05, 2007
Great, but why is the ModelUIElement3D class sealed ? The Sphere class could have been inherited from it, instead of re-implementing a Model property. Thanks, Olivier DewitAnonymous
September 06, 2007
Because ModelUIElement3D lets anyone set the model. So your Sphere class would generate a spherical model, but then absolutely anyone could come along and set it anything else. As the author of the Sphere class, you need better control of the model property, which you can do by deriving your own class and setting the Visual3DModel yourself.Anonymous
September 07, 2007
Subclassing from UIElement3D to create your own elements that respond to input, focus and eventing isAnonymous
September 10, 2007
Fala pessoal, Agora vou tentar começar a postar dicas de WPF e Silverlight com uma freqüência maior....Anonymous
September 20, 2007
I found a bug using UIElement3D's. If you use context menus somewhere in your application and while a context menu is open, you move you mouse over a UIElement3D, you get a null pointer down in System.Windows.Input.MouseDevice.PreNotifyInput(Object sender, NotifyInputEventArgs e) using .NET Reflector I have tracked down where I think the problem lies, but obviously am unable to do anything about it and I couldn't really find anywhere to post a bug report. element6 = element3 as ContentElement; element3 = InputElement.GetContainingInputElement(element6.GetUIParent(true)); those are the lines of code i believe are causing the problem (since the element3 object could be a UIElement3D... I have resigned to using a global exception handler to avoid the problem for right now i created a new project from scratch using only xaml and no custom code and get this same problem so i am quite certain it is nothing that I am doing... let me know if I can be of anymore help, and sorry, i know this isn't exactly the right place for thisAnonymous
September 20, 2007
darth -- We ended up finding that bug in our testing as well and it has been fixed for the final release. Thanks for reporting it! JordanAnonymous
November 06, 2007
There's a similar exception thrown when moving the stylus over a subclassed element on a tablet PC: System.InvalidCastException: Unable to cast object of type 'MyUIElement3D' to type 'System.Windows.ContentElement'. at System.Windows.Input.StylusLogic.UpdateOverProperty(StylusDevice stylusDevice, IInputElement newOver) at System.Windows.Input.StylusDevice.ChangeStylusOver(IInputElement stylusOver) at System.Windows.Input.StylusLogic.SelectStylusDevice(StylusDevice stylusDevice, IInputElement newOver, Boolean updateOver) at System.Windows.Input.StylusLogic.PreNotifyInput(Object sender, NotifyInputEventArgs e) at System.Windows.Input.InputManager.ProcessStagingArea() at System.Windows.Input.InputManager.ProcessInput(InputEventArgs input) at System.Windows.Input.StylusLogic.InputManagerProcessInput(Object oInput) at System.Windows.Input.StylusLogic.PreProcessInput(Object sender, PreProcessInputEventArgs e) at System.Windows.Input.InputManager.ProcessStagingArea() at System.Windows.Input.InputManager.ProcessInput(InputEventArgs input) at System.Windows.Input.InputProviderSite.ReportInput(InputReport inputReport) at System.Windows.Interop.HwndMouseInputProvider.ReportInput(IntPtr hwnd, InputMode mode, Int32 timestamp, RawMouseActions actions, Int32 x, Int32 y, Int32 wheel) at System.Windows.Interop.HwndMouseInputProvider.FilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled) at System.Windows.Interop.HwndSource.InputFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled) at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled) at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o) at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Boolean isSingleParameter) at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)Anonymous
November 12, 2007
Thanks for the stack trace, Rodney. We managed to catch this before we released and it has been fixed.Anonymous
August 03, 2009
Can we define the model declaratively as it is done it user-controls rather than doing it through code?