unable to find method in View Model

Aug 10, 2011 at 9:15 PM
Edited Aug 11, 2011 at 12:04 AM

Tony,

Can you help me understand what I am doing wrong when calling a method from a user control? Here is the rough layout of how I have my project setup

Search.xaml  <-- here I have the dataContext set using the Locator snippet
   |_SearchResultCollection.xaml  <-- this is a user control that contains a listbox with a data template and datatemplate selctor used to display results based upon the type returned

Here is the xaml for the SearchResultCollection.xaml

Note: The selection changed event within the listbox only updates a Textblock with the selected items id. I have this bound to a property RecordID within the view model. The ultimate goal is to eventually pass this id to a method that will allow me to pull the full details of the record and display it in a new view.

 

<ListBox x:Name="ResultListBox"
                 Grid.Row="1"
                 HorizontalAlignment="Stretch"
                 Background="{x:Null}"
                 BorderThickness="0"
                 HorizontalContentAlignment="Stretch"
                 ItemContainerStyle="{StaticResource ListBoxItemStyle1}"
                 ItemsSource="{Binding SearchResults[0].Results}"
                 Loaded="ResultListBox_Loaded"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                 SelectionChanged="ResultListBox_SelectionChanged"
                 Style="{StaticResource ListBoxStyle1}">

            <ListBox.ItemTemplate>

                <DataTemplate>
                    <dts:TypeTemplateSelector Content="{Binding}" HorizontalContentAlignment="Stretch">
                        <!--  CFS Template  -->
                        <formatter:TypeTemplateSelector.CFSTemplate>
                            <DataTemplate>
                                <qr:ucIndex_CFS />
                            </DataTemplate>
                        </formatter:TypeTemplateSelector.CFSTemplate>

                        <!--  Person Template  -->
                        <formatter:TypeTemplateSelector.PersonTemplate>
                            <DataTemplate>
                                <qr:ucIndex_Person />
                            </DataTemplate>
                        </formatter:TypeTemplateSelector.PersonTemplate>

                        <!--  Vehicle Template  -->
                        <formatter:TypeTemplateSelector.VehicleTemplate>
                            <DataTemplate>
                                <qr:ucIndex_Vehicle />
                            </DataTemplate>
                        </formatter:TypeTemplateSelector.VehicleTemplate>


                        <!--  Location Template  -->
                        <formatter:TypeTemplateSelector.LocationTemplate>
                            <DataTemplate>
                                <qr:ucIndex_Location />
                            </DataTemplate>
                        </formatter:TypeTemplateSelector.LocationTemplate>

                        <!--  Incident Template  -->
                        <formatter:TypeTemplateSelector.IncidentTemplate>
                            <DataTemplate>
                                <Grid x:Name="IncidentRoot"
                                      MinWidth="900"
                                      MinHeight="100"
                                      MaxHeight="110"
                                      HorizontalAlignment="Stretch">
                                    <Grid.RowDefinitions>
                                        <RowDefinition />
                                        <RowDefinition Height="2" />
                                    </Grid.RowDefinitions>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="50" />
                                        <ColumnDefinition Width="850*" />
                                    </Grid.ColumnDefinitions>
                                    <StackPanel Grid.RowSpan="2" Orientation="Vertical">
                                        <Image Width="48"
                                               Height="48"
                                               Source="/Images/search/incident.png" />
                                        <TextBlock Style="{StaticResource ResultLabel}" Text="{Binding Relevance}" />
                                        <TextBlock Style="{StaticResource ResultLabel}" Text="{Binding Agency}" />
                                    </StackPanel>
                                    <Grid Grid.Column="1" Margin="0,0,0,2">
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="0.796*" MinWidth="600" />
                                            <ColumnDefinition Width="0.204*" />
                                        </Grid.ColumnDefinitions>
                                        <StackPanel Orientation="Vertical">
                                            <HyperlinkButton HorizontalAlignment="Left"
                                                             Content="{Binding Type}"
                                                             Style="{StaticResource ListBoxtTitleHyperlink}" />
                                            <StackPanel Orientation="Horizontal">
                                                <TextBlock Style="{StaticResource ResultLabel}" Text="Incident Location: " />
                                                <TextBox HorizontalAlignment="Left"
                                                         Style="{StaticResource ResultTextBox}"
                                                         Text="{Binding Content[incidentaddress]}" />
                                            </StackPanel>
                                            <ContentControl HorizontalAlignment="Left"
                                                            Content="{Binding HitContext,
                                                                              Converter={StaticResource FormatConverter}}"
                                                            Style="{StaticResource ResultContextStyle}" />
                                        </StackPanel>
                                        <StackPanel Grid.Column="1"
                                                    Margin="0,0,0,3"
                                                    d:LayoutOverrides="Width">
                                            <StackPanel Orientation="Horizontal">
                                                <TextBlock Style="{StaticResource ResultLabel}"
                                                           Text="Incident Desc:"
                                                           d:LayoutOverrides="Width" />
                                                <TextBox Style="{StaticResource ResultTextBox}"
                                                         Text="{Binding Content[incidentdescription]}"
                                                         d:LayoutOverrides="Width" />
                                            </StackPanel>
                                            <StackPanel Orientation="Horizontal">
                                                <TextBlock Style="{StaticResource ResultLabel}"
                                                           Text="Date of Incident:"
                                                           d:LayoutOverrides="Width" />
                                                <TextBox Style="{StaticResource ResultTextBox}"
                                                         Text="{Binding Content[dateincident]}"
                                                         d:LayoutOverrides="Width" />
                                            </StackPanel>
                                            <StackPanel Orientation="Horizontal">
                                                <TextBlock Style="{StaticResource ResultLabel}"
                                                           Text="Incident No:"
                                                           d:LayoutOverrides="Width" />
                                                <TextBox Style="{StaticResource ResultTextBox}"
                                                         Text="{Binding Content[incidentnumber]}"
                                                         d:LayoutOverrides="Width" />
                                            </StackPanel>
                                            <StackPanel Orientation="Horizontal">
                                                <TextBlock Style="{StaticResource ResultLabel}"
                                                           Text="Case No:"
                                                           d:LayoutOverrides="Width" />
                                                <TextBox Style="{StaticResource ResultTextBox}"
                                                         Text="{Binding Content[casenumber]}"
                                                         d:LayoutOverrides="Width" />
                                            </StackPanel>
                                            <Button Content="Get ID">
                                                <i:Interaction.Triggers>
                                                    <i:EventTrigger EventName="Click">
                                                        <ei:CallMethodAction MethodName="GetDetailID" TargetObject="{Binding}" />
                                                    </i:EventTrigger>
                                                </i:Interaction.Triggers>
                                            </Button>
                                        </StackPanel>
                                    </Grid>
                                    <Rectangle Grid.Row="1"
                                               Grid.ColumnSpan="2"
                                               Height="1"
                                               VerticalAlignment="Bottom">
                                        <Rectangle.Fill>
                                            <LinearGradientBrush StartPoint="-0.003,16" EndPoint="1.004,16">
                                                <GradientStop Offset="0" Color="#00B2B2B2" />
                                                <GradientStop Offset="1" Color="#00B2B2B2" />
                                                <GradientStop Offset="0.25" Color="#E5B2B2B2" />
                                                <GradientStop Offset="0.75" Color="#E5B2B2B2" />
                                            </LinearGradientBrush>
                                        </Rectangle.Fill>
                                    </Rectangle>
                                </Grid>
                            </DataTemplate>
                        </formatter:TypeTemplateSelector.IncidentTemplate>
                    </dts:TypeTemplateSelector>

                </DataTemplate>
            </ListBox.ItemTemplate>

        </ListBox>

 

Each data template can display a user control based upon the type returned. I took the last data template and populated it with the xaml from the user control so you could see what one of the templates looks like.  Within the template I have a button that I have added the interaction triggers to call a method within my searchviewmodel. However, when I click on the button I get an error message stating that the method name GetDetailID on object of Type QueryResult that matches the expected... QueryResult is the  WCF service I am calling which returns SearchResults to to the listbox.  Based upon the message presented I can see that the click event/button is looking to the items source of the control (Search results) and is not able to get to the root of the view model where the method resides. 

When using data templates how do you go about getting a button within that template to regain the context of the root view model so that the method could be called?  Everything I have tried thus far has only seemed to instantiate a new instance of the view model. I don't want to do this as I am trying to use existing values from other properties such as the RecordID property.  Setting the data context of the button to the root of the view model resets the property to null ( as expected) which does me no good.

For reference here is the code used within the viewmodel and model

 

//SearchViewModel.cs
//SeachViewModel

public class SearchViewModel : ViewModelBase<SearchViewModel>
    {
	#region Properties
	private ObservableCollection<QueryResponse> _SearchResults = new ObservableCollection<QueryResponse>();
        public ObservableCollection<QueryResponse> SearchResults
        {
            get { return this._SearchResults; }
            set
            {
                if (this._SearchResults == value)
                    return;
                this._SearchResults = value;
                NotifyPropertyChanged(vm => vm.SearchResults);
            }
        }
private string recordID;
        public string RecordID
        {
            get { return recordID; }
            set
            {
                recordID = value;
                NotifyPropertyChanged(m => m.RecordID);
            }
        }

 #endregion #region Methods public void GetSearchResult() { currentPage = 1; // Search starting with the first result. SearchResults = this._DataModel.GetSearchResults(this.SearchTerm, 0); //search history items this.SearchHistory = this._DataModel.AddSearchHistoryItem(this.SearchTerm); } public void GetDetailID() { //test to see if the method could be called from the button MessageBox.Show("Record with id " + RecordID.ToString() + " selected."); } #endregion //SearchModel.cs // methods used within SearchModel // the model also contains properties but i omitted them as they reflect the same as within the VM for brevity public class SearchModel : ModelBase<SearchModel> { public ObservableCollection<QueryResponse> GetSearchResults(string searchQuery, int startDocument) { this.currentPage = generatePageNumberFromDocumentNumber(startDocument, this.resultsPerPage); SearchClient sc = new SearchClient(); sc.QueryCompleted +=new EventHandler<QueryCompletedEventArgs>(sc_QueryCompleted); sc.QueryAsync(new Query { QueryText = searchQuery, QueryStartDocument = startDocument }); return this.SearchResults; } void sc_QueryCompleted(object sender, QueryCompletedEventArgs e) { try { if (SearchResults != null) { this.SearchResults.Clear(); this.SearchResults.Add(e.Result); } else { this.SearchResults = new ObservableCollection<QueryResponse>(); this.SearchResults.Add(e.Result); } QueryResponse response = SearchResults[0]; if ((this.SearchResults[0].TotalMatchesFound % this.resultsPerPage) != 0) { this.numberOfPages = (int)(this.SearchResults[0].TotalMatchesFound / this.resultsPerPage) + 1; } else { this.numberOfPages = (int)(this.SearchResults[0].TotalMatchesFound / this.resultsPerPage); } //generateTestList(); initializePageNavigationControls(this.numberOfPages, this.currentPage, this.MAXIMUM_PAGES_DISPLAYABLE, this.NavigationButtonList); } catch(Exception ex) { ex.StackTrace.ToString(); } } }

 

Within this project I have to connect to both WCF service endpoints as well as RIA so I realize that the RIA stuff will be handled differently. However in this initial search I have to use async calls to my WCF service.

I would greatly appreciate any insight into what I am doing wring and  how to get a button placed within a data template to call a method within the view model.

Thanks in advance,

Cheers,

Coordinator
Aug 11, 2011 at 1:24 AM
Quick answer off the top of my head ... There are certain situations where you need to set the Source of a binding explicitly, rather than pick it up from the ambient DataContext. In these scenarios what I do is instead create a ViewModel in the resource dictionary of the View and give it a key. Then bind the event trigger to that View using StaticResource and the key. I'll follow this up tomorrow with an example.

Cheers,
Tony
Sent from my phone

>
Coordinator
Aug 12, 2011 at 12:42 PM

I've put together an example of creating a ViewModel as a resource in the View.  The reason why you might need to sometimes do this is that you need to set the Source of a Binding, rather than getting it from the ambient DataContext.  One example of this is binding the Visibility of a column in a DataGrid.  Because a column is nested within a DataGrid, it does not have access to the same DataContext the grid does.  So you can't leave out the Source property of the Binding.

The solution is to create the ViewModel as a resource in the View.

<UserControl.Resources>
    <my:CustomerViewModel x:Key="vm"  xmlns:my="clr-namespace:ViewModelAsResource"/>
</UserControl.Resources>

A binding would then look like this:

<TextBox Grid.Row="0" Grid.Column="1" Height="30"
    Text="{Binding Source={StaticResource vm}, Path=Model.CustomerId}" />

The problem with this is that the default constructor of the ViewModel is called when it is created as a resource.  So if your ViewModel uses a service agent, you'll need to create a property for the service agent.

private ICustomerServiceAgent serviceAgent;
public ICustomerServiceAgent ServiceAgent
{
    get { return serviceAgent; }
    set { serviceAgent = value; }
}

Then you'll set the ServiceAgent property in the code-behind of the View (instead of via a Locator).

public partial class CustomerView : UserControl
{
    public CustomerView()
    {
        InitializeComponent();

        // Get ViewModel from resource and set service agent
        var vm = (CustomerViewModel)Resources["vm"];
        vm.ServiceAgent = new MockCustomerServiceAgent();
        DataContext = vm;
    }
}

This approach is not as clean as using a locator, but it allows you to set the Source of a binding explicitly.  Here is a link to a sample project.

Hope this helps!

Cheers,

Tony

Aug 12, 2011 at 3:37 PM

Tony, Thanks for the response and the sample. What you present makes since however I do not see how this is different from setting the datacontext of a view as you would normally do. I must be doing something wrong since I still seem to have the issue that the listbox items contain a different datacontext than the view (which I realize is the norm). Using a static resource on the button seems to only instantiate a new instance of the viewmodel which wipes out all previous properties that I would like to use as parameters in my methods.

I may be implementing this incorrectly but here is what I have thus far within the user control I created the reference

 <UserControl.Resources>
        <formatter:HighlightConverter x:Key="FormatConverter" />
        <vml:SearchViewModel x:Key="vm" />
    </UserControl.Resources>
and then within my button i explicitly set the reference to the vm

 <HyperlinkButton HorizontalAlignment="Left"
       Click="Button_Click"
       Content="{Binding Type}"
       Style="{StaticResource ListBoxtTitleHyperlink}">
    <i:Interaction.Triggers>
       <i:EventTrigger EventName="Click">
          <ei:CallMethodAction MethodName="GetDetailID" TargetObject="{Binding Source={StaticResource vm}}" />
       </i:EventTrigger>
    </i:Interaction.Triggers>
 </HyperlinkButton>

Setting it up this way does allow me to access the method however, this seems to be instantiating a new VM as all previous properties are set to null. I realize this may be beyond the scope of SimpleMVVM and more of an MVVM implementation issue. I ran into this prior to using SimpleMVVM and I was hoping that the locator within SimpleMVVM would help to resolve this scenario where items within a listbox would have access to the same view model as that of the view.

Is there are means to achieve this within SimpleMVVM or should I look to delgate commands /commanding as a means to accomplish this task?

Thanks again for all your help and guidance!

Cheers

Coordinator
Aug 13, 2011 at 10:07 PM

Open the sample app I created and take a look at the code-behind for the View.

public partial class CustomerView : UserControl
{
    public CustomerView()
    {
        InitializeComponent();

        // Get ViewModel from resource and set service agent
        var vm = (CustomerViewModel)Resources["vm"];
        vm.ServiceAgent = new MockCustomerServiceAgent();
        DataContext = vm;
    }
}

First, notice that I have a ServiceAgent property on the ViewModel that I am setting.  This allows you to call methods on the service agent to populate properties on the ViewModel.  In the sample I'm doing this on the NewCustomer method, but you may want to do it on a method that is called when the View loads.

Second, notice that I am setting the DataContext of the view to the ViewModel that was created as a resource for the View.  This allows other controls to use the DataContext by omitting the Source property of the Binding.

Hope this helps!

Cheers,

Tony

Aug 18, 2011 at 7:24 PM
Edited Aug 18, 2011 at 7:24 PM

Tony,

I wanted to post an update to this question as I got a working solution. The issue I was having before was that a new view model was being instantiated when the view was created. I needed to find a solution that would enable me to access the same view model that was created when the view was created vs. generating a new one. Your suggestion above was very close but I was still finding that a new ViewModel was being generated when the button was clicked which caused all properties within the view model to be reset to null. I addressed this with Delegate commands and Command parameters but I wanted to figure out how to get triggers to work in this scenario.

What I found was that I needed to set the DataContext for the view in the code behind of the view. I removed all settings in the xaml and instead set it in the constructor for the view.  Similar to your suggestion above however the one change that needed to be made for it to work in my situation was to set the resource and DataContext  BEFORE the view was initialized.  Setting it after would fail as the resource needed to be set/defined before the view was created. 

 

    public partial class TemplateView : UserControl
    {
        public TemplateView()
        {
            var templateViewModel = ((BindingHelper.Locators.ViewModelLocator)App.Current.Resources["Locator"]).TemplateViewModel;
            this.DataContext = templateViewModel;
            this.Resources.Add("vm", templateViewModel);
            InitializeComponent();
}

Then within the xaml I was able set my static resource for the button as:

<i:Interaction.Triggers>
      <i:EventTrigger EventName="Click">
           <ei:CallMethodAction  TargetObject="{Binding Source={StaticResource vm}}" MethodName="ShowId"/>
       </i:EventTrigger>
</i:Interaction.Triggers>
 

 

Setting the data context in this manner the buttons which were nested within a data template where able to locate the method in the root view model and call the appropriate methods. This was a HUGE benefit as I now have access to other properties within the VM that may be set by other objects.

Hopefully this may be of benefit to others.

Thanks again for your help!