EventToCommand in DataTemplate

Jan 11, 2010 at 3:23 PM

In the sample below, I am trying to use the EventToCommand behavior in a DataTemplate. The issue is that the DataContext for the DataTemplate is the data from an ItemSource.

That works fine for sending the CommandParameters, but the Binding for Command is messed up. I don't know how to specify the binding path for the top level control for which this data template is being used in.

 

SelectMarker is a RelayMethod in the ViewModel, but how do I get a binding handle for the ViewModel, when the DataTemplate has been given a different context.

 

 

 

Any ideas?

 

    <DataTemplate x:Key="EventTemplate">
    	<Border Width="10" 
    		Height="10" 
    		map:MapLayer.Position="{Binding MapLocation}"
    		map:MapLayer.PositionOrigin="Center"
    		Tag="{Binding Name}" 
    		ToolTipService.ToolTip="{Binding Name}"
    		Opacity="1" 
    		UseLayoutRounding="false" 
    		Background="Red" 
    		CornerRadius="10" 
             >
    		<i:Interaction.Triggers>
    			<i:EventTrigger EventName="MouseLeftButtonUp">
    				<GalaSoft_MvvmLight_Command:EventToCommand 
					Command="{Binding SelectMarker}" 
					CommandParameter="{Binding ID}"/>
    			</i:EventTrigger>
    		</i:Interaction.Triggers>
    	</Border>
    </DataTemplate>

Coordinator
Jan 12, 2010 at 8:24 AM

Hi,

The DataContext in data templates is automatically set on the data item that the template renders, as you found out. However, don't forget that DataContext is really just a shortcut. You can set a Binding to another object by specifying the Source of the Binding explicitly.

For example:

Command="{Binding Source={StaticResource Locator}, Path=Main.SelectMarker}"

Hope that helps,

Laurent

Jan 12, 2010 at 10:05 AM

You have an excellent talent of succinctly summarizing the issue. Thank you.

Let me try to return the favor.

The VM is bound to the DataContext of the TemplateParent by the Locator dynamically. So what I am looking for is a way from within the Template to relatively crawl up to the TemplateParent, get the DataContext value, and crawl the PATH to get SelectMarker object (which is a RelayCommand).

Alternatively, I suppose that if the RelayCommand was a static resource in "Main" called Main.SelectMarker then I could certainly use the Locator to find that RelayCommand and bind it to the template.

What I don't think is possible is passing an reference to "this" to the Locator Main.SelectMarker get property method, so that that method could then crawl the tree, find the parent and then return the SelectMarker method that is appropriate for this particular View.







-----Original Message-----
From: lbugnion [mailto:notifications@codeplex.com]
Sent: Tue 1/12/2010 4:24 AM
To: Gutfreund, Yechezkal
Subject: Re: EventToCommand in DataTemplate [mvvmlight:80447]

From: lbugnion

Hi,The DataContext in data templates is automatically set on the data item that the template renders, as you found out. However, don't forget that DataContext is really just a shortcut. You can set a Binding to another object by specifying the Source of the Binding explicitly.For example:Command="{Binding Source={StaticResource Locator}, Path=Main.SelectMarker}"Hope that helps,Laurent



Coordinator
Jan 12, 2010 at 10:28 AM
Edited Jan 12, 2010 at 10:30 AM

Hi,

I was actually hoping this would be a solution (and actually it is if you bind your ViewModel to the View through the ViewModelLocator like it is proposed by default in the toolkit when you create a new MVVM Light application. The Main property is a gateway to the MainViewModel through the ViewModelLocator class, which is declared as a resource (in App.xaml). In the current implementation. Commands are properties, so the expression {Binding Source={StaticResource Locator}, Path=Main.SelectMarkerCommand} binds the button's Command property to that particular command on the MainViewModel instance (the command does *not* have to be static, it is a normal property). It is just an indirection, if you like, since we cannot use the DataContext directly.

Another way to do this, if you don't have the ViewModelLocator, is to use the TemplatedParent from within the template to access the templated control. Then you can get the DataContext from there. This works, however, only from SIlverlight 4. Is that an option for you?

I would be happy to make you a sample if you like. Let me know.

Cheers,

Laurent

Jan 12, 2010 at 2:15 PM

It's close to a solution. But I need to fill you in a little bit on what I did to your ViewModelLocator.

In my application there is One instance of a Model (Class EventModel) and many dynamically created (V,VM) pairs created. The Class of the VM is always Class DataLens, but different Classes for each View.

The View is wired to the View Model by:

          DataContext="{Binding DataLens, Source={StaticResource Locator}}"

And the Locator does:

public DataLens DataLens
        {
            get { return new DataLens(); }
        }

So there is no MainStatic.

So I am looking for something I can use in the DataTemplate that can crawl up and find the current INSTANCE of the DataLens and bind to it. I don't think this works..and SL does not have WPF relative path binding.

{Binding RelativeSource={RelativeSource TemplatedParent}, Path=DataContext.SelectRelayCommand}

This is the creation of the RelayCommand in DataLens:
SelectMarker = new RelayCommand<string>(idString => this.selectMarker(idString));


Alternatively, I can try to create a Static property in the DataLens class that holds the RelayCommand, and then try to figure out from the sender and event flags who it is that was pressed, and what the parent DataContext is.

Does that sound right?

 

 

 

 

Coordinator
Jan 12, 2010 at 3:20 PM

Ah OK that makes sense.

Silverlight 4 does have RelativeSource={RelativeSource TemplatedParent}. Is it an option for you to upgrade?

I am not sure you'll be able to bind to a static property, Silverlight does not have x:Static. That might be tough too. Another way could be to bind someting as a CommandParameter (for example just {Binding} which returns the current item) but again this is only an option in SL4, unless you do a few tricks like I did in the SL3 implementation of EventTocommand.

if I was the architect on this project, and unless you have really good reasons not to, I would upgrade to SL4.

Cheers,

Laurent

Jan 12, 2010 at 3:43 PM

I am using SL4, but I am stuck on the syntax for the Relative Source that I should be using..

 

Coordinator
Jan 12, 2010 at 6:42 PM

Ah yes I was mistaken. TemplatedParent refers to the Marker in that case, not to the Lens.

I have another solution: Can you save a reference to the Lens when you create the Markers? If yes, you can do

<DataTemplate x:Key="LensItemTemplate">
    <StackPanel Orientation="Horizontal">
        <Button Content="Click"
                Width="75"
                Command="{Binding 
                            Path=Parent.SelectMarkerCommand}"
                CommandParameter="{Binding}" />
        <TextBlock TextWrapping="Wrap"
                   Text="{Binding Name}"
                   VerticalAlignment="Center"
                   Margin="10,0,0,0" />
    </StackPanel>
</DataTemplate>

with Parent being a property within the Marker class, that stores a reference to the LensViewModel.

public LensViewModel(int lensIndex)
{
    Markers = new ObservableCollection<Marker>();

    for (var index = 0; index < 3; index++)
    {
        var marker = new Marker(this)
        {
            Name = "Lens " + lensIndex + " Marker " + index
        };

        Markers.Add(marker);
    }

    SelectMarkerCommand = new RelayCommand<Marker>(m => MessageBox.Show(m.Name));
}

and
public class Marker
{
    public LensViewModel Parent
    {
        get;
        private set;
    }

    public string Name { get; set; }

    public Marker(LensViewModel parent)
    {
        Parent = parent;
    }
}

 
Hopefully that is possible for you.
Cheers,
Laurent
Jan 12, 2010 at 9:01 PM

The Markers are created automatically from ItemSource by BingMaps. That is why they are data Templates:

                <map:MapLayer x:Name="eventLayer"

                            Visibility="{Binding EventDisplay, Converter={StaticResource Bool2Visible}, Mode=OneWay}" >

                    <map:MapItemsControl

                                  ItemTemplate="{StaticResource UEventTemplate}"

                                  ItemsSource="{Binding EventData, Mode=OneWay}"

                                  HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"

                        />

                </map:MapLayer>

The current template I am using is not MVVM friendly. It works. But I have to have an event handler in the View:

            <DataTemplate x:Key="UEventTemplate">

                <Border

                    Width="10"

                     Height="10"

                     map:MapLayer.Position="{Binding MapLocation}"

                     map:MapLayer.PositionOrigin="Center"

                     Tag="{Binding ID}"

                     ToolTipService.ToolTip="{Binding Name}"

                     Opacity="1"

                     UseLayoutRounding="false"

                     Background="Red"

                    MouseLeftButtonUp="doSelect"

                     CornerRadius="{Binding Selected, Converter={StaticResource Bool2Radius}, Mode=OneWay}"

                     >

                </Border>

            </DataTemplate>

The Handler looks like this:

        private void doSelect(object sender, MouseEventArgs e)

        {

            Border markerBox = sender as Border;

            string id = markerBox.Tag as string;

            DataLens lens = this.DataContext as DataLens;

            lens.selectMarker(id);

        }

        #end

This is very ugly. What I want to do is something that uses your EventToCommand Behavior, and not call DataLens.selectMarker() directly, but rather invoke the RelayCommand called SelectMarker:

    <DataTemplate x:Key="EventTemplate">

       <Border Width="10"

              Height="10"

              map:MapLayer.Position="{Binding MapLocation}"

              map:MapLayer.PositionOrigin="Center"

              Tag="{Binding ID}"

              ToolTipService.ToolTip="{Binding Name}"

              Opacity="1"

              UseLayoutRounding="false"

              Background="Red"

            MouseLeftButtonUp="doSelect"

              CornerRadius="10"

             >

              <!--

                    

                           <GalaSoft_MvvmLight_Command:EventToCommand

                                  Command="{Binding SelectMarker}"

                                  CommandParameter="{Binding ID}"/>

                    

              -->

       </Border>

    </DataTemplate>

From: lbugnion [mailto:notifications@codeplex.com]
Sent: Tuesday, January 12, 2010 2:43 PM
To: Gutfreund, Yechezkal
Subject: Re: EventToCommand in DataTemplate [mvvmlight:80447]

From: lbugnion

Ah yes I was mistaken. TemplatedParent refers to the Marker in that case, not to the Lens.

I have another solution: Can you save a reference to the Lens when you create the Markers? If yes, you can do

<DataTemplate x:Key="LensItemTemplate">
    <StackPanel Orientation="Horizontal">
        <Button Content="Click"
                Width="75"
                Command="{Binding 
                            Path=Parent.SelectMarkerCommand}"
                CommandParameter="{Binding}" />
        <TextBlock TextWrapping="Wrap"
                   Text="{Binding Name}"
                   VerticalAlignment="Center"
                   Margin="10,0,0,0" />
    </StackPanel>
</DataTemplate>
 
 
 

with Parent being a property within the Marker class, that stores a reference to the LensViewModel.

public LensViewModel(int lensIndex)
{
    Markers = new ObservableCollection<Marker>();
 
    for (var index = 0; index < 3; index++)
    {
        var marker = new Marker(this)
        {
            Name = "Lens " + lensIndex + " Marker " + index
        };
 
        Markers.Add(marker);
    }
 
    SelectMarkerCommand = new RelayCommand<Marker>(m => MessageBox.Show(m.Name));
}
 
and
public class Marker
{
    public LensViewModel Parent
    {
        get;
        private set;
    }
 
    public string Name { get; set; }
 
    public Marker(LensViewModel parent)
    {
        Parent = parent;
    }
}
 
 
 
 
Hopefully that is possible for you.
Cheers,
Laurent

Read the full discussion online.

To add a post to this discussion, reply to this email (mvvmlight@discussions.codeplex.com)

To start a new discussion for this project, email mvvmlight@discussions.codeplex.com

You are receiving this email because you subscribed to this discussion on CodePlex. You can unsubscribe on CodePlex.com.

Please note: Images and attachments will be removed from emails. Any posts to this discussion will also be available online at CodePlex.com

Jan 13, 2010 at 4:29 PM

After a good nights sleep, all became clearer. While not optimal in all aspects, your first advice in this chain was the best. Have the Locator find a Static ViewModel.

So now I have a centeral VM dispatcher for all events that I want to capture from DataTemplates. Events are arriving there via the attached EventToCommand Behavior. This way the template can live anywhere. With explicit MouseEvent fields, the XAML parsing failed at runtime because the template was not in the xaml as the code behind.

The simple version of a data template for a marker looks like this

    <DataTemplate x:Key="EventTemplate">
    	<Border 
		   Width="10" 
    		Height="10" 
    		map:MapLayer.Position="{Binding MapLocation}"
    		map:MapLayer.PositionOrigin="Center"
    		Tag="{Binding ID}" 
    		ToolTipService.ToolTip="{Binding Name}"
    		Opacity="1" 
    		UseLayoutRounding="false" 
    		Background="Red" 
    		CornerRadius="{Binding Selected, Converter={StaticResource Bool2Radius}, Mode=OneWay}"
             >
    		<i:Interaction.Triggers>
    			<i:EventTrigger EventName="MouseLeftButtonDown">
    				<gala:EventToCommand 
					Command="{Binding Source={StaticResource Locator}, Path=ToolPanel.SelectMarker}"
					CommandParameter="{Binding ID}"/>
    			</i:EventTrigger>
    		</i:Interaction.Triggers>
    	</Border>
    </DataTemplate>