1

I have the following xaml view:

<UserControl x:Class="MyViews.PersonView"
  xmlns:views="clr-namespace:MyViews"
 [...]
>
[...]
<dxb:BarManager x:Name="MainBarManager">
  <dxb:BarManager.Items>
    <dxb:BarButtonItem x:Name="bbiPrint"
                       Content="{Binding Print, Source={StaticResource CommonResources}}" 
                       Command="{Binding PrintPersonsCommand}" 
                       CommandParameter="{Binding PersonsCardView, ElementName=CardUserControl}"
                       />
  </dxb:BarManager.Items>
  <Grid>
    <Grid.RowDefinitions>
    [...]
    </Grid.RowDefinitions>
    <views:CardView x:Name="CardUserControl" Grid.Row="2"/>
  </Grid>
[...]
</UserControl>

The CardView is defined as follows:

<UserControl x:Class="MyViews.CardView"
             [...]>
[...]

    <dxg:GridControl ItemsSource="{Binding Persons}" SelectedItems="{Binding SelectedPersons}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" SelectionMode="MultipleRow">
        [...]
        <dxg:GridControl.View>
            <dxg:CardView x:Name="PersonsCardView" 
                          [...]
                          CardTemplate="{StaticResource DisplayCardTemplate}" 
                          PrintCardViewItemTemplate="{StaticResource PrintCardTemplate}"/>
        </dxg:GridControl.View>
        [...]
    </dxg:GridControl>
</UserControl>

The PrintPersonsCommand is defined as follows in my ViewModel:

public class PersonViewModel
{
  public PersonViewModel(...)
  {
    [...]
    PrintPersonsCommand = new Prism.Commands.DelegateCommand<DataViewBase>(PrintPersons, CanPrintPersons);
  }  

  public Prism.Commands.DelegateCommand<DataViewBase> PrintPersonsCommand { get; private set; }

  private void PrintPersons(DataViewBase view)
  {
    _printService.ShowGridViewPrintPreview(view);
  }

  private bool CanPrintPersons(DataViewBase view)
  {
    return true;
  }
}

Now, when I click the Print button, the above PrintPersons method is always fed with null. How do I pass CardUserControl.PersonsCardView in my MyViews.PersonView xaml above, how do I pass that PersonCardView to my command? In other words, how do I fix

CommandParameter="{Binding PersonsCardView, ElementName=CardUserControl}"

to make it work?

Currently, the only solution I've found to this problem is to replace the Command and CommandParameter with

ItemClick="OnPrintBtnClick"

and then in the PersonView's code-behind file to do:

private void OnPrintBtnClick(object sender, ItemClickEventArgs e)
{
  var ctxt = DataContext as PersonViewModel;
  ctxt.PrintPersonsCommand.Execute(CardUserControl.PersonsCardView);
}

That works but I can't believe there is no other way. I'm not happy with that solution because I don't have the benefits of using the Command any more, like e.g. the automatic evaluation of the Command's CanExecute method. I could also put the CardView's xaml code in the PersonView.xaml but I like my controls to be in separate files because I have the feeling it's more structured and each user control has its own responsibilities which can nicely be split into separate files. Also, that solution binds my view to my view model too tightly.

Can someone help me out please?

Laurent Michel
  • 1,069
  • 3
  • 14
  • 29
  • What do you need the parameter for? I.e. what's in the `PersonsCardView` that's not in the `PersonViewModel`? – Haukinger Sep 19 '19 at 08:52
  • I want to print the cards of my card view. I have other views, like a grid view. The `PersonViewModel` has no information about that `PersonsCardView`. It only carries the necessary logic to print a view. I don't want the `PersonViewModel` to have any kind of information on the view to be printed. It would bind the view model to the view too tightly. – Laurent Michel Sep 19 '19 at 08:55
  • My `PersonView`'s child views (like `CardView` mentioned above and `GridView` not mentioned above) all share the same view model. – Laurent Michel Sep 19 '19 at 08:56
  • Fair point, but _passing the view as parameter to the view model_ looks like the tightest coupling I can imagine, already. – Haukinger Sep 19 '19 at 11:10
  • No, because I am passing an abstraction of it. Both the `CardView` and the `GridView` inherit from `DataViewBase`. I do not expect any of the views used in my app to be of a different type. – Laurent Michel Sep 19 '19 at 11:33

2 Answers2

2

Without changing your existing view and viewmodel hierarchy, I was able to pass the GridControl.View to the PersonViewModel using the Tag property You can assign the CardView to the Tag property at the bottom of your CardView UserControl, and then access this Tag as CommandParameter.

CardView UserControl

<UserControl x:Class="MyViews.CardView"
         [...]>
    [...]

<dxg:GridControl ItemsSource="{Binding Persons}" SelectedItems="{Binding SelectedPersons}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" SelectionMode="MultipleRow">
    [...]
    <dxg:GridControl.View>
        <dxg:CardView x:Name="PersonsCardView" 
                      [...]
                      CardTemplate="{StaticResource DisplayCardTemplate}" 
                      PrintCardViewItemTemplate="{StaticResource PrintCardTemplate}"/>
    </dxg:GridControl.View>
    [...]
</dxg:GridControl>

<UserControl.Tag>
    <Binding ElementName="PersonsCardView"/>
</UserControl.Tag>

</UserControl>

Print Button Xaml:

<dxb:BarButtonItem x:Name="bbiPrint"
                   Content="{Binding Print, Source={StaticResource CommonResources}}" 
                   Command="{Binding PrintPersonsCommand}" 
                   CommandParameter="{Binding ElementName=CardUserControl, Path=Tag}"
                   />
Insane
  • 654
  • 10
  • 18
  • That works perfectly. Is that way to proceed a standard in WPF or am I doing my stuff in the wrong way? Is there a better way to organize my UserControls? – Laurent Michel Sep 19 '19 at 11:42
  • 1
    Passing a UI Control to the viewmodel is generally not encouraged in MVVM. If we have to just print the view, then we could do it in code behind, as printing a control has nothing to do with the viewmodel. Or, If printing has to be initiated in the viewmodel, then I would have placed the Print command in the CardViewModel. It would make the passing of the UI element much more cleaner just by using CommandParameter as binding. – Insane Sep 19 '19 at 11:53
  • I think I get your point; I'll post an answer with my understanding of your both suggestions – Laurent Michel Sep 19 '19 at 12:08
0

Based on the valuable input of Insane, I came up with the following two cleaner fixes:

Code-behind solution

In the PersonView, use the ItemClick event handler on the Print button:

<dxb:BarButtonItem x:Name="bbiPrint"
  Content="{Binding Print, Source={StaticResource CommonResources}}" 
  ItemClick="OnPrintBtnClick"/>

Adapt the corresponding code-behind file like this:

public partial class PersonView : UserControl
{
  readonly IPrintService _printService;

  public PersonView(IPrintService printService)
  {
    _printService = printService;

    InitializeComponent();
  }

  private void OnPrintBtnClick(object sender, ItemClickEventArgs e)
  {
    _printService.ShowGridViewPrintPreview(CardUserControl.PersonsCardView);
  }
}

Because I want to gray-out the Print button when there is no selection, I still need to add some code to make that happen. I can get it by 1. updating the button code to

<dxb:BarButtonItem x:Name="bbiPrint"
  Content="{Binding Print, Source={StaticResource CommonResources}}" 
  ItemClick="OnPrintBtnClick" IsEnabled="{Binding CanPrintPersons}"/> 
  1. refreshing the CanPrintPersons property in the PersonViewModel upon Persons selection change

That's it.

CardViewModel solution

In that solution, we have a PersonView with its underlying PersonViewModel and a CardView with its underlying CardViewModel. I will not describe that solution with all the details as it is overkill in my situation but for the sake of completeness, I'll give the main points. Upon clicking the Print button on the PersonView, the PersonViewModel's PrintCommand is called. That command emits a Print event to the CardViewModel which in turn calls its own PrintCommand. That latter command calls

_printService.ShowGridViewPrintPreview(View);

where the View is a CardViewModel's property that is set upon CardView loading with e.g.

<dxmvvm:Interaction.Behaviors>
  <dxmvvm:EventToCommand EventName="Loaded" Command="{Binding ViewLoadedCommand}" CommandParameter="{Binding ElementName=PersonsCardView}" />
</dxmvvm:Interaction.Behaviors>

Because I have two child views I want to print, I'd need to add a view model for each one of those. In addition, those two view models plus the PersonViewModel need access to the list of Persons to be printed. In particular, they need a shared access to the same data, so that they are synchronized. A simple way to do that is explained here and is totally doable. But I think it is not worth the trouble for the simple use case I have as it adds more complexity than necessary.

Laurent Michel
  • 1,069
  • 3
  • 14
  • 29