Select to view content in your preferred language

MouseMove Event & MVVM

3839
10
10-13-2011 06:31 AM
BenTreptow1
Deactivated User
I'm trying to display the mouse cursor coordinate location in a text box on my map. Easy to do with a MouseMove event and the Map.ScreenToMap() method in the code behind, not so easily done using MVVM though. This is because sending the "sender" object (the Map control in this case) to the view model makes the the view model "aware" of the view and essentially defeats the purpose of using the MVVM pattern. Plus, from what I understand about MVVM, you don't want controls in your vm eithers.

Does anybody have any suggestions on a better way to do this?
0 Kudos
10 Replies
BrandonCopeland
Emerging Contributor
Are the Map and coordinate TextBox in the same View or in 2 different Views?

Knowing only about these 2 controls and no other features of your application, I would have them in the same view. This is simply because the TextBox involves no additional presentation logic, model data, or unique feature set. Its just an additional control displaying the same data as your map but in a different way.

So lets tackle that option (both in the same view). Essentially, your TextBox would like to display some information from another control in the UI. This is no different than if it wanted to display the Width of the map. No need to have the View Model intervene there - just bind one control to the other. The only problem is mouse coordinates are not publicly exposed like Width. We need to find some way to expose those from the Map. A Behavior is a valid option. You could create a Behavior with a property (or properties) to describe mouse coordinates and bind your TextBox to that property.

If they are in different Views, the idea could still be the same. Now, you just need some way of communicating the mouse coordinates between Views (View Models). You could TwoWay bind the coordinates to your View Model that is updating some underlying model. Implement some decoupled way of sharing that model like Messenger/Event Aggregator or a common Service. Expose as a property on the second View Model and bind your TextBox to that.
0 Kudos
BenTreptow1
Deactivated User
Brandon,

Thanks for the advice. I have mouse move coordinate behavior created and it's working the way it should. I'm not sure how to bind the value coming out of my behavior to the my textblock. Could you give me a little more advice on how do that?

Thanks,

Ben

    
<Grid x:Name="LayoutRoot">
        <esri:Map x:Name="map_BaseMap" Layers="{Binding Path=MapLayers}" helpers:MapHelper.ZoomGeometry="{Binding Path=MapExtent}" MinimumResolution=".25">
            <esri:Map.Extent>
                <esri:Envelope XMin="-10510990.377700" YMin="5586273.272800" XMax="-10364472.557100" YMax="5682307.505400">
                    <esri:Envelope.SpatialReference>
                        <esri:SpatialReference WKID="102100"/>
                    </esri:Envelope.SpatialReference>
                </esri:Envelope>
            </esri:Map.Extent>
            <interactivity:Interaction.Behaviors>
                <behaviors:MouseCoordinateBehavior/>
            </interactivity:Interaction.Behaviors>
        </esri:Map>
        <controls:LogisNavigation Margin="5" Map="{Binding ElementName=map_BaseMap}" FullExtentGeometry="{Binding Path=FullExtent}" 
                                  InitialExtentGeometry="{Binding Path=InitialExtent}" helpers:NavigationHelper.ZoomReset="{Binding Path=MapExtent}" 
                                  Background="Gray" BorderBrush="White" Foreground="Black"/>
        <esri:MapProgressBar HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,5" Width="150" Map="{Binding ElementName=map_BaseMap}"/>
        <controls:LogisScaleBar HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="5" Width="200" MapUnit="Meters" 
                                Map="{Binding ElementName=map_BaseMap}"/>
        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Top" Margin="5" Foreground="White"/>
    </Grid>


using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;
using ESRI.ArcGIS.Client;
using ESRI.ArcGIS.Client.Geometry;
using ESRI.ArcGIS.Client.Projection;

namespace MVVM_Debug.Behaviors
{
    public class MouseCoordinateBehavior : Behavior<Map>
    {
        #region Constructors
        
        public MouseCoordinateBehavior()
            : base()
        {

        }

        #endregion

        #region Properties

        public string MouseCoordinates { get; private set; }

        #endregion

        #region Methods
        
        protected override void OnAttached()
        {
            base.OnAttached();

            this.AssociatedObject.MouseMove += new MouseEventHandler(AssociatedObject_MouseMove);
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            this.AssociatedObject.MouseMove -= new MouseEventHandler(AssociatedObject_MouseMove);
        }

        void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
        {
            Map map = (Map)sender;
            Point mouseLoc = e.GetPosition(map);
            MapPoint coordLoc = map.ScreenToMap(mouseLoc);

            if (coordLoc != null)
            {
                WebMercator webMercator = new WebMercator();
                MapPoint latLon = webMercator.ToGeographic(coordLoc) as MapPoint;

                MouseCoordinates = string.Format("Lon {0:F4}° Lat {1:F4}°", latLon.X, latLon.Y);
            }
        }

        #endregion
    }
}
0 Kudos
DarinaTchountcheva
Frequent Contributor
Ben,

You could probably use a TargetedTriggerAction instead of Behavior since you don't need to store any state between the actions.

The code would look like this:

public class MouseCoordinatesAction :TargetedTriggerAction<TextBlock>
    {
        /// <summary>
        /// Invokes the action.
        /// Search for the user provided address
        /// </summary>
        /// <param name="parameter">The parameter to the action. If the Action does not require a parameter, the parameter may be set to a null reference.</param>
        protected override void Invoke(object parameter)
        {
            if ((Target != null) && (parameter != null))
                Target.Text = GetCoordinateString(parameter as MouseEventArgs);
        }

        private string GetCoordinateString(MouseEventArgs e)
        {
            string mouseCoordString = string.Empty;
            Map map = AssociatedObject as Map;
            Point mouseLoc = e.GetPosition(map);
            MapPoint coordLoc = map.ScreenToMap(mouseLoc);

            if (coordLoc != null)
            {
                WebMercator webMercator = new WebMercator();
                MapPoint latLon = webMercator.ToGeographic(coordLoc) as MapPoint;

                mouseCoordString = string.Format("Lon {0:F4}° Lat {1:F4}°", latLon.X, latLon.Y);
            }

            return mouseCoordString;
        }
    }


<Grid x:Name="LayoutRoot">
        <esri:Map x:Name="map_BaseMap" Layers="{Binding Path=MapLayers}" helpers:MapHelper.ZoomGeometry="{Binding Path=MapExtent}" MinimumResolution=".25">
            <esri:Map.Extent>
                <esri:Envelope XMin="-10510990.377700" YMin="5586273.272800" XMax="-10364472.557100" YMax="5682307.505400">
                    <esri:Envelope.SpatialReference>
                        <esri:SpatialReference WKID="102100"/>
                    </esri:Envelope.SpatialReference>
                </esri:Envelope>
            </esri:Map.Extent>
            <interactivity:Interaction.Triggers>
                <interaction:EventTrigger EventName="MouseMove">
                    <actions:MouseCoordinatesAction TargetName="mouseCoordinates" />
                </i:EventTrigger>
        </esri:Map>
        <controls:LogisNavigation Margin="5" Map="{Binding ElementName=map_BaseMap}" FullExtentGeometry="{Binding Path=FullExtent}" 
                                  InitialExtentGeometry="{Binding Path=InitialExtent}" helpers:NavigationHelper.ZoomReset="{Binding Path=MapExtent}" 
                                  Background="Gray" BorderBrush="White" Foreground="Black"/>
        <esri:MapProgressBar HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,5" Width="150" Map="{Binding ElementName=map_BaseMap}"/>
        <controls:LogisScaleBar HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="5" Width="200" MapUnit="Meters" 
                                Map="{Binding ElementName=map_BaseMap}"/>
        <TextBlock x:Name="mouseCoordinates" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="5" Foreground="White"/>
    </Grid>


Add a namespace in your xaml file
xmlns:interaction="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"

And the TargetName of your action is the name of the TextBlock, where you want to display the coordinates.

Good Luck!
0 Kudos
BenTreptow1
Deactivated User
Darina,

Thank you for the suggestion, this method definitely worked. However, in my application the coordinates will unltimately be displayed in some other control (not simply a textblock) and may need to be displayed else where in the application too. The targetedtriggeraction seems kind of limited in that regard since you have to specify the type <T> of action it is. Is there any way to make this more dynamic so that mouse coordinates can be displayed in not only a single textblock?

Thanks
0 Kudos
BrandonCopeland
Emerging Contributor
Darina makes a good point. This situation is better suited for a TriggerAction than a behavior. A nice solution would be an action to update a property on another object when invoked. Luckily, Microsoft already did this for us with the ChangePropertyAction in the Blend SDK. Using ChangePropertyAction, you can bind to TextBlock.Text or a property on your View Model - whatever you like. We do need to extend it and do 1 small override for our coordinate calculation.

It ends up looking like...

using System.Windows;
using System.Windows.Input;
using ESRI.ArcGIS.Client;
using ESRI.ArcGIS.Client.Geometry;
using ESRI.ArcGIS.Client.Projection;
using Microsoft.Expression.Interactivity.Core;

public class CoordinateChangePropertyAction : ChangePropertyAction
{
    protected override void Invoke(object parameter)
    {
        this.Value = GetCoordinateString(parameter as MouseEventArgs);
        base.Invoke(parameter);
    }

    private string GetCoordinateString(MouseEventArgs e)
    {
        string mouseCoordString = string.Empty;
        Map map = AssociatedObject as Map;
        Point mouseLoc = e.GetPosition(map);
        MapPoint coordLoc = map.ScreenToMap(mouseLoc);

        if (coordLoc != null)
        {
            WebMercator webMercator = new WebMercator();
            MapPoint latLon = webMercator.ToGeographic(coordLoc) as MapPoint;

            mouseCoordString = string.Format("Lon {0:F4}° Lat {1:F4}°", latLon.X, latLon.Y);
        }

        return mouseCoordString;
    }
}


and the XAML... (this is assuming you have a LayoutRoot bound to your view model)

<esri:Map x:Name="map_BaseMap">
             <i:Interaction.Triggers>
              <i:EventTrigger EventName="MouseMove">
               <MyActionNamespace:CoordinateChangePropertyAction TargetObject="{Binding DataContext, ElementName=LayoutRoot}" PropertyName="SomePropertyOnTheViewModelToBindTo"/>
              </i:EventTrigger>
             </i:Interaction.Triggers>
</esri:Map>


That's it - now you have it in your view model. Expose it in whatever view model is supporting your TextBlock.

Of course if ChangePropertyAction is not available for your project or you can't use the Blend SDK for any reason, you could still use Darina's action from above. You would just need change the Type from TextBlock to whatever fits for you (your View Model perhaps or some abstraction of it). Instead of updating the .Text property, you would update the needed property on that object.

One suggestion. You may want to consider eliminating the formatting to string from the action. Just return a MapPoint. Leave it up to the TextBlock displaying the data to decide how it should be formatted. Its really simple to do so using Binding's StringFormat property or IValueConverter.
0 Kudos
BenTreptow1
Deactivated User
I have my ChangePropertyAction setup, but I think I'm still missing something because I can't get it to work. It looks like the VM property (Coordinates) is never being set by the action. I'm new to MVVM and actions in general, so perhaps I'm totally missing how this is suppose to work. Or I'm just missing something really obvious. Thanks for the help!

<UserControl x:Class="MVVM_Debug.MainPage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:interactivity="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:interactions="http://schemas.microsoft.com/expression/2010/interactions"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:esri="http://schemas.esri.com/arcgis/client/2009"
             xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL4"
             xmlns:actions="clr-namespace:MVVM_Debug.Actions"
             xmlns:behaviors="clr-namespace:MVVM_Debug.Behaviors"
             xmlns:controls="clr-namespace:MVVM_Debug.Controls"
             xmlns:helpers="clr-namespace:MVVM_Debug.Helpers"
             xmlns:toolkit="clr-namespace:MVVM_Debug.Toolkit"
             mc:Ignorable="d" d:DesignHeight="600" d:DesignWidth="800"
             DataContext="{Binding Main, Source={StaticResource Locator}}">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Skins/MainSkin.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot">
        <esri:Map x:Name="map_BaseMap" Layers="{Binding Path=MapLayers}" helpers:MapHelper.ZoomGeometry="{Binding Path=MapExtent}" MinimumResolution=".25">
            <esri:Map.Extent>
                <esri:Envelope XMin="-10510990.377700" YMin="5586273.272800" XMax="-10364472.557100" YMax="5682307.505400">
                    <esri:Envelope.SpatialReference>
                        <esri:SpatialReference WKID="102100"/>
                    </esri:Envelope.SpatialReference>
                </esri:Envelope>
            </esri:Map.Extent>
            <interactivity:Interaction.Triggers>
                <interactivity:EventTrigger EventName="MouseMove">
                    <actions:MouseCoordinateAction TargetObject="{Binding Path=DataContext, ElementName=LayoutRoot}" 
                                                   PropertyName="{Binding Path=Coordinates}"/>
                </interactivity:EventTrigger>
            </interactivity:Interaction.Triggers>
        </esri:Map>
        <controls:LogisNavigation Margin="5" Map="{Binding ElementName=map_BaseMap}" FullExtentGeometry="{Binding Path=FullExtent}" 
                                  InitialExtentGeometry="{Binding Path=InitialExtent}" helpers:NavigationHelper.ZoomReset="{Binding Path=MapExtent}" 
                                  Background="Gray" BorderBrush="White" Foreground="Black"/>
        <esri:MapProgressBar HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,5" Width="150" Map="{Binding ElementName=map_BaseMap}"/>
        <controls:LogisScaleBar HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="5" Width="200" MapUnit="Meters" 
                                Map="{Binding ElementName=map_BaseMap}"/>
        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Top" Margin="5" Foreground="White" Text="{Binding Path=Coordinates.X, StringFormat=\{0:F4\}}"/>
    </Grid>
</UserControl>


using System.Windows;
using System.Windows.Input;
using ESRI.ArcGIS.Client;
using ESRI.ArcGIS.Client.Geometry;
using ESRI.ArcGIS.Client.Projection;
using Microsoft.Expression.Interactivity.Core;

namespace MVVM_Debug.Actions
{
    public class MouseCoordinateAction : ChangePropertyAction
    {
        protected override void Invoke(object parameter)
        {
            this.Value = GetLatLon(parameter as MouseEventArgs);
            base.Invoke(parameter);
        }

        private MapPoint GetLatLon(MouseEventArgs e)
        {
            Map map = AssociatedObject as Map;
            Point mouseLoc = e.GetPosition(map);
            MapPoint coordLoc = map.ScreenToMap(mouseLoc);

            if (coordLoc != null && coordLoc.Extent.SpatialReference.WKID == 102100)
            {
                WebMercator webMercator = new WebMercator();
                MapPoint latLon = webMercator.ToGeographic(coordLoc) as MapPoint;

                return latLon;
            }

            else
                return null;
        }
    }
}
0 Kudos
DarinaTchountcheva
Frequent Contributor
Brandon,

This is a very interesting and cool approach. Thank you for sharing!
I am still trying to wrap my head around MVVM. I am sure that I am not alone.
That's why I would like to continue this discussion. I believe, it will help us all in our struggles with MVVM.

I see a few other ways of achieving this. I will post them here, and I would love to get some input from experienced MVVM developers: what is a MVVM "Yes, yes", and what is a MVVM "No, no".
Note: I agree with Brandon that using a MapPoint instead of string is a better approach, but I use string for less code in the samples.


1. Using the Messenger - LightMvvm

public class MouseCoordinatesAction : TriggerAction<Map>
    {
        /// <summary>
        /// Invokes the action.
        /// Search for the user provided address
        /// </summary>
        /// <param name="parameter">The parameter to the action. If the Action does not require a parameter, the parameter may be set to a null reference.</param>
        protected override void Invoke(object parameter)
        {             
            Messenger.Default.Send(GetCoordinateString(parameter as MouseEventArgs), "FromMainViewModel");
        }

        private string GetCoordinateString(MouseEventArgs e)
        {
            //this is the body of the method, but I will remove it to make the code shorter

            return mouseCoordString;
        }
       
    }



//**************************** Inside MainViewModel *************************


 //A string with the current mouse point coordinates
        private string _currentMapPoint;


        /// <summary>
        /// A string with the current mouse point coordinates
        /// </summary>
        public string CurrentMapPoint
        {
            get { return _currentMapPoint; }
            set
            {
                if (_currentMapPoint != value)
                {
                    _currentMapPoint = value;
                    OnNotifyPropertyChanged("CurrentMapPoint");
                }
            }
        }


 public MainViewModel()
        {
            
     //requires a reference to GalaSoft.MvvmLight.SL4
            Messenger.Default.Register<string>(this, "FromMainViewModel", (e => this.CurrentMapPoint = e));
            
        }

//********************** In MainView xaml - Trigger is inside the Map control***********************


 <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseMove">
                    <actions:MouseCoordinatesAction  />
                </i:EventTrigger>
        </i:Interaction.Triggers>


 <TextBlock Text="{Binding CurrentMapPoint}"
            x:Name="mouseCoordinates" Width="200" Height="22" 
            VerticalAlignment="Bottom" HorizontalAlignment="Right"/>


2. Using Brandon's suggestion to use the ViewModel as a target of the Action:

public class MouseCoordinatesAction : TargetedTriggerAction<MainViewModel>
    {
        /// <summary>
        /// Invokes the action.
        /// Search for the user provided address
        /// </summary>
        /// <param name="parameter">The parameter to the action. If the Action does not require a parameter, the parameter may be set to a null reference.</param>
        protected override void Invoke(object parameter)
        {           
            Target.CurrentMapPoint = GetCoordinateString(parameter as MouseEventArgs);            
        }

        private string GetCoordinateString(MouseEventArgs e)
        {
            //removing the body for shortness

            return mouseCoordString;
        }
       
    }


//**************************** Inside MainViewModel *************************


 //A string with the current mouse point coordinates
        private string _currentMapPoint;


        /// <summary>
        /// A string with the current mouse point coordinates
        /// </summary>
        public string CurrentMapPoint
        {
            get { return _currentMapPoint; }
            set
            {
                if (_currentMapPoint != value)
                {
                    _currentMapPoint = value;
                    OnNotifyPropertyChanged("CurrentMapPoint");
                }
            }
        }

//********************** In MainView xaml - Trigger is inside the Map control***********************


 <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseMove">
                    <actions:MouseCoordinatesAction  TargetObject="{Binding DataContext, ElementName=LayoutRoot}" />
                </i:EventTrigger>
        </i:Interaction.Triggers>


 <TextBlock Text="{Binding CurrentMapPoint}"
            x:Name="mouseCoordinates" Width="200" Height="22" 
            VerticalAlignment="Bottom" HorizontalAlignment="Right"/>


3. Using a dependency property inside the Action:

public class MouseCoordinatesAction : TriggerAction<Map>
    {
        /// <summary>
        /// Invokes the action.
        /// Search for the user provided address
        /// </summary>
        /// <param name="parameter">The parameter to the action. If the Action does not require a parameter, the parameter may be set to a null reference.</param>
        protected override void Invoke(object parameter)
        {           
            MousePoint = GetCoordinateString(parameter as MouseEventArgs);            
        }

        private string GetCoordinateString(MouseEventArgs e)
        {
            //removing code for shortness

            return mouseCoordString;
        }


        /// <summary>
        /// The symbol used to display the addresss point buffer on the map
        /// </summary>
        public string MousePoint
        {
            get { return (string)GetValue(MousePointProperty); }
            set { SetValue(MousePointProperty, value); }
        }

        public static readonly DependencyProperty MousePointProperty =
            DependencyProperty.Register("MousePoint", typeof(string), typeof(MouseCoordinatesAction), null);
    }


//**************************** Inside MainViewModel *************************


 //A string with the current mouse point coordinates
        private string _currentMapPoint;


        /// <summary>
        /// A string with the current mouse point coordinates
        /// </summary>
        public string CurrentMapPoint
        {
            get { return _currentMapPoint; }
            set
            {
                if (_currentMapPoint != value)
                {
                    _currentMapPoint = value;
                    OnNotifyPropertyChanged("CurrentMapPoint");
                }
            }
        }

//********************** In MainView xaml - Trigger is inside the Map control***********************


 <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseMove">
                    <actions:MouseCoordinatesAction  MousePoint="{Binding CurrentMapPoint,Mode=TwoWay" />
                </i:EventTrigger>
        </i:Interaction.Triggers>


 <TextBlock Text="{Binding CurrentMapPoint}"
            x:Name="mouseCoordinates" Width="200" Height="22" 
            VerticalAlignment="Bottom" HorizontalAlignment="Right"/>


Thank you!

Darina
0 Kudos
DarinaTchountcheva
Frequent Contributor
Ben,

Change PropertyName="{Binding Path=Coordinates}" to PropertyName="Coordinates", and it will work. 🙂
0 Kudos
BenTreptow1
Deactivated User
Darina & Brandon,

Thanks for all the great information, I have it working now! Hopefully this will spark some new conversations about MVVM here, it's new to me as well. I'm in the process of writing a rather large application using MVVM for the first time. I like it, but there are quite a few challenges to overcome.

Thanks,

Ben
0 Kudos