Welcome to WindowsClient.net | Sign in | Join

 

Sponsors





  • advertise here

Tags

No tags have been created or used yet.
Connecting the ViewModel Results (Part 1)

In my last post I laid out the criteria for our experiment. With this already defined we can now construct our hypothesis:


Hypothesis

If I construct a View [View is the independent variable], then the ViewModel of the View will change [ViewModel is the dependent variable]


Getting the ViewModel

When I started thinking about how to do this I knew that I wanted it to be simple, preferably a one liner in the View to link the ViewModel. I decided that writing a behavior via an Attached Property. I wanted to do something like below:

<UserControl xmlns:behaviors="clr-namespace:Project.Behaviors" behaviors:ViewModelResolver.HasViewModel="True"/>

Looking at the above, how would we possibly do this. Well, we would have to do a few things. First we would have to assume convention where the View is appended with View and the matching ViewModel has the same name as the View yet appended with ViewModel. For example: ShellView and ShellViewModel. The next thing we would have to do is reflect over the assemblies to find the matching ViewModel.

To start out we must first create the attached property. We will need to have just one property and we will also need to attach to the property change event so that we can wire up the ViewModel. That property looks like this:

public static readonly DependencyProperty HasViewModelProperty = DependencyProperty.RegisterAttached("HasViewModel", typeof(bool), typeof(ViewModelResolver), new PropertyMetadata(false, OnHasViewModelChanged));

As we said, we create the attached property and we also wire up the property change event. In the property change event we are going to restrict the DependencyObject to be a UserControl. Below is the property change event handler:

private static void OnHasViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { UserControl uc = d as UserControl; string viewModelName = default(string); if (uc != null) { if((bool)e.NewValue) { viewModelName = string.Format("{0}{1}", uc.GetType().Name, "Model"); uc.DataContext = GetViewModel(viewModelName); } else { uc.DataContext = null; } } }

What we do here is first check to make sure the DependencyObject is a UserControl. You should feel free to change this if you’d like, however, the convention is that the View is a UserControl. Next, we check to see if e.NewValue is true. If it is we figure out the ViewModel to use for the controls DataContext. We first get the name of the control from the type and append Model. As we said earlier, we are assuming convention here where ViewModel is appended to the name of the View. If the control is named ShellView, we append ‘Model’ and we end up with ShellViewModel. If it is false, we set the DataContext to null for that control. Fairly simple.

The important step here is the GetViewModel method which is listed below:

private static object GetViewModel(string viewModelName) { IEnumerable<Assembly> assemblies = AppDomain.CurrentDomain.GetAssemblies(); object vm = null; foreach (Assembly assembly in assemblies) { foreach (Type type in assembly.GetExportedTypes()) { if (string.Compare(type.Name, viewModelName, StringComparison.Ordinal) == 0) { vm = DependencyResolver.Container.Resolve(type); break; } } if (vm != null) { break; } } return vm; }

This code in itself is fairly simple. First we use reflection to enumerate over the assemblies. Then for each assembly we enumerate over the types to find our ViewModel. I’m using Unity in this example to grab the ViewModel but you can use any method you want here to retrieve the ViewModel. You can grab it from a store, create an instance of it, etc.

The GetViewModel method, while fairly quick to implement, is a bit overkill. There could also be some name collision happening where the ViewModel is retrieved from the wrong assembly. In the next post we will examine how to improve this design.

Part 2

Connecting the ViewModel Results (Part 2)

My last post showed how to implement a DependencyProperty to wire up the ViewModel automatically using reflection. It was noted that the GetViewModel method in the last post was not an ideal solution since we lose some control over what assemblies are enumerated over. We can get around this by adding a list of known assemblies we want to enumerate over. to do this will we just create a simple collection that we can use to add assemblies by name.


Improving the GetViewModel method

To start we need to inform the code that we may only want to enumerate over types in certain assemblies. We can add a simple property to the ViewModelResolver class to keep track of these assemblies:

public static ICollection<string> LookupAssemblyNames { get { return baseLookupAssemblyNames; } }

We can now use this property to add only the assembly names we care about. In our App.xaml code behind file we will override the OnStartup method and populate the LookupAssemblyNames collection with only the types we care about:

protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); Behaviors.ViewModelResolver.LookupAssemblyNames.Add("ConnectingTheViewModel"); }

We are now telling the ViewModelResolver to only look for the assembly named ‘ConnectingTheViewModel’. This mean we will only enumerate over the types in that assembly. We can change the GetViewModel method to now look like this:

private static object GetViewModel(string viewModelName) { IEnumerable<Assembly> assemblies = GetLookupAssemblies(); object vm = null; foreach (Assembly assembly in assemblies) { foreach (Type type in assembly.GetExportedTypes()) { if (string.Compare(type.Name, viewModelName, StringComparison.Ordinal) == 0) { vm = DependencyResolver.Container.Resolve(type); break; } } if (vm != null) { break; } } return vm; }

As you can now see we need one more method to complete the code. This method is called GetLookupAssemblies and it looks like this:

private static IEnumerable<Assembly> GetLookupAssemblies() { Assembly[] currentAssemblies = AppDomain.CurrentDomain.GetAssemblies(); IEnumerable<Assembly> assemblies = (from string name in baseLookupAssemblyNames from Assembly assembly in currentAssemblies where string.Compare(assembly.GetName().Name, name, StringComparison.Ordinal) == 0 select assembly); return assemblies != null ? assemblies : currentAssemblies.AsEnumerable(); }

The above code is first getting a list of the assebmlies and is then using some Linq to get the assemblies that match and item in the LookupAssemblyNames collection.


While the code is fairly simple in design and can definitely be improved upon it satisfies all criteris for our experiment. It does not use ViewModelLocator, it does not use MEF, it is easy to use with one line of xaml setting the attached property and it requires little or no configuration. Remember that code and ideas here are not written in stone and are used as the sole purpose of experiments in what you can achieve when you think about the problem a bit differently. I do realize the cost of reflection can be high but I also believe the cost is acceptable.

Download the source code to this experiment

Connecting the ViewModel - Problem

Recently I was working on yet another WPF project, this one was a personal project outside of work. I’m a big proponent of the MVVM pattern in applications. If you are unfamiliar with MVVM you can check out more information on this subject here.

Where there is a lot of guidance and opinion on MVVM there is also a lot of freedom in the design. Some people prefer no code in the View, some don’t care. Some people let the View create the ViewModel, others don’t. It’s all relative I guess. I personally prefer a clean View without any code. This creates a problem of linking the ViewModel to the View. Without any code in the View how do you map the two. More specifically, how do you map the two easily and without a lot of code or a bunch of xaml.

While there are a few idea out there about doing this, for example the ViewModelLocator, I didn’t like creating StaticResources just to create the ViewModel. In the example linked to above it just felt like there were a few too many steps. Why doesn’t my View just know it has a ViewModel? As I thought about this more I figured it would make a perfect experiment post and maybe get another perspective on the problem. As always, these posts are mainly for experimentation. The criteria I came up with for this experiment are below:

  • No ViewModelLocator
  • Exclusion of MEF
  • Must be easy to use
  • Must require as little configuration as necessary

Let us now go through the criteria in a bit more detail.

  • No ViewModelLocator

I didn’t want to use the ViewModelLocator because I didn’t want to take the dependency on MEF. I also wanted to try to think of a different way to accomplish the same task. I’m not saying ViewModelLocator is a bad design, it is quite good in fact, it just didn’t feel right to me.

  • Exclusion of MEF

I don’t believe you need to use MEF in order to accomplish this. This is something I wanted to exclude so we can maybe get an idea of how to do this without using another technology, such as MEF.

  • Must be easy to use

The method must be fairly painless to implement. A bunch of steps required or a bunch of xaml just to wire things up would defeat the purpose of the exepriment.

  • Must require as little configuration as necessary.

Again keeping in line with the previous criteria, the configuration must not be a bunch of xaml or a bunch of data in app.config. It should be fairly easy to configure and get running.


The next post will look into how we will go about implementing the experiment.

Panning and Zooming Experiment Results (Part 2)

Hopefully my last post didn't completely fry your brain. I wanted to mention that I understand that linear interpolation has some inaccuracy with it, but for this experiment it is a perfectly viable solution. Just wanted to throw that out there in case anyone was questioning the issue.


Zoom, Zoom Zoom!

Let's move on to the zoom portion of the experiment. There is a lot to discuss here so please bear with me. Remembering the first post, we defined some criteria to adhere to with respect to zooming. I will list those here so we can refer to these from this page:

  • The control must be able to animate between states and have an option for turning zoom animations off
  • The zooming must work with the mouse wheel and we must be able to set the direction for zoom in/out
  • The control needs to support basic key combinations with mouse clicks. A key state for zoom in, a key state for zoom out, a key state for center, and a key state for reset
  • The control must zoom to the mouse position

Some of these things we will not go into detail about because they are pretty simple switches we invoke on the actual code. The Dependency Properties in the source code will point you in the right direction.

Now, before we nose dive into this portion, one important decision was not yet discussed. We know the child element will sit inside of a parent container, but what will this container be? I thought about this a bit and tried a few things out. I wanted to make sure that only one child was able to be added and that the child would be responsible for its children. That rules out Panel. Although technically it could be done with a Panel, I tend to think of a Panel as having multiple children which is not what I wanted.  What we also want to avoid is the parent scaling with the child. A Decorator is the perfect parent container for this control. With that said, I could have went along the path of implementing my own custom Decorator. I did not do this. Why reinvent the wheel when a perfectly working Decorator already exists; Border. So, the control will inherit from Border.

If you have seen or used the Silverlight MultiScaleImage control, the zooming feature is smooth and pleasant. We are trying to shoot for that same experience with our control. Once again we will defer to linear interpolation for our equations. We will also target the CompositionTarget.Rendering event for animating per frame. This time though we will hit some problems when we scale, especially with respect to the criteria we specified dealing with zooming to the mouse position.

Originally, and before I actually started coding this control, I thought I would just set the CenterX and CenterY properties on the ScaleTransform and be on my merry way. As I later found out, I was very wrong. While CenterX and CenterY work perfectly fine, they do not cooperate when zooming to the mouse. The diagram below illustrates the problem.

panzoom_zoomproblem

Let me explain the different colors and what is happening here. The red squares represent the child element we want to scale. The yellow squares are the position of the mouse when we start to scale. The semi-transparent red squares are where the child element moves when you set the CenterX and CenterY properties on the ScaleTransform in relation to the mouse.

Now let's talk about why this happens. Remember in my last post where I mentioned that the ActualHeight and ActualWidth do not change when using a RenderTransform to scale. So if we use the 300 X 300 rectangle size again, in the image on the right above, the CenterX and CenterY is set to about 300. This repositions the element when you scale it. I kept working on this and tried to apply a TranslateTransform after the scale, but it was jerky at best. I did wind up using a TranslateTransform in the end, but I discarded the CenterX and CenterY properties. And that is what we will move on to.

We need two sets of reference points. One being the location of the mouse inside of the parent container, and the other being the location of the mouse inside of the child container. We want the scaled location of the child element in the end, not the actual location. For this we can use the zoom scale and calculate where this is, but for now we will grab the actual location. To grab the points we use the Mouse class and call GetPosition:

ParentZoomPoint = Mouse.GetPosition(Parent)
ChildZoomPoint = Mouse.GetPosition(Child)

Now that we know where the mouse is in relation to the two elements, we need to figure out where we want to zoom to. In order for this to be accurate, we must scale the ChildZoomPoint using the scale factor. We then take the result of this and subtract it from the ParentZoomPoint in order to get our final zoom position. The way we do this is with the following:

ZoomPosition.X = ParentZoomPoint.X - (ChildZoomPoint.X x ZoomScale)
ZoomPosition.Y = ParentZoomPoint.Y - (ChildZoomPoint.Y x ZoomScale)

We now know exactly where we need to zoom to, but we still have yet to talk about what factor we need to scale to. I have a few Dependency Properties in the project, the three that are important here are: ZoomScale, ZoomScaleFactor and ZoomRatioPerAnimatedFrame. ZoomScale represents the current scale of the child element. ZoomScaleFactor is the percentage we want to increase or decrease each time we zoom, and ZoomRatioPerAnimatedFrame is the percentage of the change we want to apply on each render frame. Confused? Hopefully not.

You may be thinking to yourself, "Well, all of these values are right there for the taking, this is easy, I just take the ZoomScale, add or subtract the ZoomScaleFactor and I'm off." This is not necessarily incorrect, but there are a few problems with it. The first problem I will discuss is something I will term absolute scaling, for lack of a better definition at this point. Take for example that we have a ZoomScale of 1.0 (or 100%) and we have a ZoomScaleFactor of 0.5 (or 50%). If we just lop off the ZoomScaleFactor from the original scale each time someone zooms out, we only then have 2 levels of zoom. 1.0 to 0.5 and 0.5 to 0. Obviously this is not a very good solution. What we really want to do is only subtract or add the ZoomScaleFactor to the current scale. So we would then have n numbers of magnitude. Scaling would then turn out to be something like: 1.0 to 0.5, 0.5 to 0.25, 0.25 to 0.125 etc. The second problem is that we need to zoom to the mouse position, which we will discuss in a bit. So first lets get the difference between the current ZoomScale and where we want to scale to:

ZoomTo = ZoomScale + (ZoomScaleFactor x ZoomScale)
or
ZoomTo = ZoomScale - (ZoomScaleFactor x ZoomScale)

The first calculates when zooming in, the second calculates for zooming out. You can check the e.Delta property on the MouseWheel event and reverse these for what happens when the mouse wheel is pulled or pushed, meeting one of our zooming criteria.

Once we have all of the values we need, ZoomPosition and ZoomTo, we can next think about how to put this all together. Remember we want smooth transitions, nothing jarring or unpleasant. This is where ZoomRatioPerAnimatedFrame comes into play. This property will tell the frame how much of the ZoomTo to eat up each time it fires. The way to calculate this interpolation would be the following:

ZoomScale += (ZoomTo - ZoomScale) x ZoomRatioPerAnimatedFrame

That is the easy part, and that also satisfies the criteria for smooth zooming. The only problem left now is that the child element will not move and reposition itself when it is zooming. We stated in our criteria that the control must zoom to the mouse. So how do we do this and get around the CenterX and CenterY problem we talked about. We call on TranslateTransform to help us out here. We use the same kind of smoothing calculation as we did above and we also will use the ZoomRatioPerAnimatedFrame to keep the translation in sync with the scaling, which is very important. That would look like this:

Child.TranslateTransform.X += (ZoomPosition.X - Child.TranslateTransform.X) x ZoomRatioPerAnimatedFrame
Child.TranslateTransform.Y += (ZoomPosition.Y - Child.TranslateTransform.Y) x ZoomRatioPerAnimatedFrame

This will give us a smooth translation to the correct point and will meet our criteria for zooming to the mouse position.


Now I know I didn't talk about the overlay element, but these posts were more about the actual zooming and panning of the control. You can check out the overlay behavior in the source code. This concludes our first experiment here, and, hopefully has given you some insight into some different forms of panning and zooming! Below is the link to download the source code, remember this was just an experiment into what was maybe possible. This should not be considered production code.

Download the source code to this experiment

Panning and Zooming Experiment Results (Part 1)

Welcome back. So in my last post I laid out the criteria for our control. With this already defined we can now construct our hypothesis:


Hypothesis

If I zoom the control [zoom factor is the independent variable], then the scale of the child element will change [scale is the dependent variable]

If I pan the control [mouse position is the independent variable], then the location of the child will change in relation to its position in the parent container [position is the dependent variable]


Panning Around

The first part we will discuss is the panning of the control, or rather, the panning of the child element inside of the control. As I discussed in the previous post, we laid out some criteria for panning. One of those was the complete abandonment of the ScrollViewer. We also want a fluid movement in relation to the mouse position. There are a couple of things to consider here. We need to use some kind of animation to pan the child element. We also need to worry about the scale of that child element inside of the parent. We will talk about the former of those first.

Animating the pan could be accomplished in a variety of ways. We could use a Storyboard and just call Begin every frame, but that is not the route we will take here. Enter CompositionTarget.Rendering. This is the event we are going to attach to for our smooth transitions in between positions. Now, at first I attempted to do this in the MouseMove event, with undesired results. Using the Rendering event lets us animate once per frame which is exactly what we are looking for.

The next thing we need to look into is the scale of the child element while we are panning. You might be asking yourself, "Why do I care about the scale when I am panning?" Good question, and I will answer that. I'll quickly jump ahead and talk about scale a little bit here to put it into context, but more on this subject will be discussed in the Part 2 post which will follow this. When you scale an element in WPF with a RenderTransform, the height and width of the element does not change. For example, if I have a 300 X 300 rectangle and I set a scale of 0.5, the ActualWidth and ActualHeight are actually still reported as 300 X 300. This is the expected behavior, since we only adjust the scale and not the size of the element, and we are using a RenderTransform. As I said I will discuss this further in my next post. Moving on. Now that we know what scale means in terms of the child element, we still have to answer how this relates to panning. If we have a child element and we start panning based on a mathematical calculation, ignoring the scale will cause the element to have varying speeds at different scale factors. The higher the scale factor, the slower the pan. The lower the scale factor, the faster the pan. This is obviously not what we are looking for. We need to know the size of the element at the current zoom scale. This is easy enough to figure out with the following calculation:

ScaledWidth = Child.ActualWidth x ZoomScale
ScaledHeight = Child.ActualHeight x ZoomScale

So using our previous 300 X 300 rectangle and a zoom scale of 0.5 we would end up with the equation:

150 = 300 x 0.5 -> Width
150 = 300 x 0.5 -> Height

This alone does not completely solve our problem with pan speed related to zoom scale, but we will get to that. In order for us to provide a fluid, non jarring experience, we need to use a linear interpolation equation on the panning between points. Let's start talking in 2D space and create a situation where someone clicks down on the mouse at position x=1, y =1. This would be 1 unit from the left and one unit from the top of the control. We will call this position A. The user drags to a position of x=50, y=50. We will call this position B. What should we do here? Well we certainly don't want to just subtract B from A, get 49, snap the Child over and pat ourselves on the back. This needs to be a linear interpolation conversion from point A to B, this will give us a fluid movement. The equation that I came up with for fixing the pan speed is listed below:

x += ((B - A) / ScaledWidth) x ScaleFactor
y += ((B - A) / ScaledHeight) x ScaleFactor

Now, looking at that equation you might be asking: "Why did we multiply the scale factor back on?" The answer to this is simple - consistency. This will correct the problem of the control panning faster at lower scale factors and slower at higher scale factors. So when the scale factor is lower, we multiply it and basically say: "Hey, this is where it needs to go but slow it down a bit", and reverse for a higher scale factor. Now that we have consistent pan speeds at varying scale factors, we need to talk about linear interpolated movements. We need to tell the panning calculation to only take so much of the distance per frame. We could come up with a constant and just pop that in there, but maybe someone prefers a faster panning or a slower panning, or maybe they want to change this dynamically in relation to the zoom factor. What we need to add on here is an accelerator. So we change the equation to include this:

x += (((B - A) / ScaledWidth) x ScaleFactor) x PanAcceleratorX
y += (((B - A) / ScaledHeight) x ScaleFactor) x PanAcceleratorY

This lets the user customize the actual speed of the pan and also provides us with our smooth movements, so long as the accelerator ratio is less than the actual distance between points B and A. I won't go into discussing how this is implemented inside of the control directly, as you will be able to consume all of that when you get the source code, but this is the basic premise for panning without a ScrollViewer in this experiment. In Part 2 I will discuss the Zoom component of the control.

Part 2 - Zooming

Panning and Zooming Experiment - Problem

Ok, lets dig in. In this first post I will lay out the criteria for this experiment. I was thinking about a problem I was facing at work with an application we were using with a timeline. What I wanted to do was to allow a different form of navigation compared to what was currently available. I like how the MultiScaleImage control works in Silverlight so I thought about emulating that behavior in WPF somehow, save for the tiling. I thought about how this could be done for a few days and came up with a few ideas. I also had a set of criteria that I wanted to conform to. Panning and zooming in the general form is fairly easy to do in WPF, and this wouldn't be an experiment if we just took what was available, slapped it into a control and called it a day. So I put together a list of things that must be met for this experiment:

  • No ScrollViewer
  • The control must be able to animate between states and have an option for turning zoom animations off
  • The zooming must work with the mouse wheel and we must be able to set the direction for zoom in/out
  • The control needs to support basic key combinations with mouse clicks. A key state for zoom in, a key state for zoom out, a key state for center, and a key state for reset
  • The control must zoom to the mouse position
  • The control must support an overlay element
  • The control must be ready to go once it is placed into a project

Let me now just explain the criteria in a little more detail.

  • No ScrollViewer

I wanted to exclude ScrollViewer because it is fairly easy to do panning inside of this control. This blog is all about experimenting so I wanted to see what else I could come up with, so using this control is out of the question.

  • The control must be able to animate between states and have an option for turning zoom animations off

I didn't want to just set a zoom scale and have the element just pop into place. That is not a very good user experience and is quite jarring. On the other hand, there may come a need where animations are not wanted, so we need a switch. Also, the panning of the element must be fluid in nature, no snapping.

  • The zooming must work with the mouse wheel and we must be able to set the direction for zoom in/out

The zoom must work with the mouse wheel. With that said, I know some people have differing opinions on what the mouse wheel should do when moved in a certain direction. Some prefer that when pulling back on the mouse wheel, the zoom increases, and vice versa, so we must support that.

  • The control needs to support basic key combinations with mouse clicks. A key state for zoom in, a key state for zoom out, a key state for center, and a key state for reset

Along with the zooming using the mouse wheel, we must also support basic key combinations for zooming in and zooming out. Shift+Click should zoom in and Ctrl+Click should zoom out. We also need to be able to center an element in the control, so we will use Alt+Click for this. A simple DoubleClick will result in a reset state.

  • The control must zoom to the mouse position

Zooming to a control, as I said before, is fairly easy to do in WPF. Set the ScaleTransform and away you go. Aside from being jerky in a dynamic environment, this also brings up the question: Where do I zoom to? Now I know that you can specify a CenterX and CenterY property but this will cause problems, as you will see in the next post.

  • The control must support an overlay element

This is one that I thought about for a bit. What happens if we want to overlay a control on top of this control? Maybe we would like to add a nice gradient or maybe a logo that is always on top of this control. For this we must support an overlay.

  • The control must be ready to go once its placed into a project

Once I drag and drop this control on to the project workspace it should be ready to go. I should not have to worry about hooking up any commands or have to worry about processing any events. I want to be able to bind the overlay property to one of my controls (like a logo or something), set the child element and be ready to go. The control should know how to prepare itself with or without an overlay element and be ready for zooming and panning.


Now with all of the criteria listed, we will actually talk about how this is all implemented. I wanted to go over what we wanted to achieve first and explain the requirements I have imposed on this experiment. In my next post I will discuss each of these items and how they were integrated. Finally, I will post the final source code of this experiment. Stay tuned!

Part 1 - Panning
Part 2 - Zooming

Hello, and Welcome!

Welcome to the WPF Experiment! Before I start blogging I wanted to just touch base with everyone and explain what this blog will be about. As the name implies, I will be blogging about my experiments with WPF. This will include any random things I may come up with in my head, and also, some of the things I come across on a daily basis at work. Be forewarned that some things may be off the wall and some things you may wind up scratching your head at. The point of this whole thing is to help you maybe think outside of the box when it comes to approaching some of the things you may want to do with the technology. Things posted here may not always be the simplest way to do things and they may not be the only way. We are going to experiment here and see what happens. There are a lot of good WPF blogs out there and hopefully this will be one of the many resources available to you. I have a subject for our first experiment, and that is going to be a panning and zooming control, but unlike the ones I have seen so far. I will be posting that here shortly. So with all of that said, lets experiment!

Page view counter