I am trying to reproduce what is suggested in Sheridan's answer to this question to navigate trough my views when using WPF with the MVVM pattern. Unfortunately, I am getting a binding error when I do so. Here is the exact error:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='JollyFinance.ViewModels.MainViewModel', AncestorLevel='1''. BindingExpression:Path=DataContext.DisplayTest; DataItem=null; target element is 'Button' (Name=''); target property is 'Command' (type 'ICommand')
When I look into my xaml code in LoginView.xaml, I noticed that Visual Studio is telling me that it cannot find DataContext.DisplayText in context of type MainViewModel. I have tried removing DataContext. and just keeping DisplayText instead, but to no avail.
Unless Sheridan's answer has an error, I am most definitely missing something here. What should I do for it to work?
MainWindow.xaml:
<Window x:Class="JollyFinance.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:JollyFinance.ViewModels"
xmlns:views="clr-namespace:JollyFinance.Views"
Title="JollyFinance!" Height="720" Width="1280">
<Window.Resources>
<!-- Different pages -->
<DataTemplate DataType="{x:Type vm:LoginViewModel}">
<views:LoginView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:TestViewModel}">
<views:Test/>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Grid>
<ContentControl Content="{Binding CurrentViewModel}"/>
</Grid>
</Window>
MainViewModel.cs:
public class MainViewModel : BindableObject
{
private ViewModelNavigationBase _currentViewModel;
public MainViewModel()
{
CurrentViewModel = new LoginViewModel();
}
public ICommand DisplayTest
{
get
{
// This is added just to see if the ICommand is actually called when I press the
// Create New User button
Window popup = new Window();
popup.ShowDialog();
// View model that doesn't contain anything for now
return new RelayCommand(action => CurrentViewModel = new TestViewModel());
}
}
public ViewModelNavigationBase CurrentViewModel
{
get { return _currentViewModel; }
set
{
if (_currentViewModel != value)
{
_currentViewModel = value;
RaisePropertyChanged("CurrentViewModel");
}
}
}
}
LoginView.xaml:
<UserControl x:Class="JollyFinance.Views.LoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:JollyFinance.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<vm:LoginViewModel/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Username: " Grid.Column="1" Grid.Row="1" Margin="5"/>
<TextBox Text="{Binding Path=Username}" Grid.Column="2" Grid.Row="1" Grid.ColumnSpan="2" Margin="5"/>
<TextBlock Text="Password: " Grid.Column="1" Grid.Row="2" Margin="5"/>
<PasswordBox x:Name="PasswordBox" PasswordChar="*" Grid.Column="2" Grid.ColumnSpan="2" Grid.Row="2" Margin="5"/>
<Button Content="Log In" Grid.Column="2" Grid.Row="3" Margin="5" Padding="5" Command="{Binding LoginCommand}"/>
<Button Content="Create new user" Grid.Column="3" Grid.Row="3" Margin="5" Padding="5"
Command="{Binding DataContext.DisplayTest, RelativeSource={RelativeSource AncestorType={x:Type vm:MainViewModel}},
Mode=OneWay}"/>
</Grid>
</UserControl>
LoginViewModel.cs:
public class LoginViewModel : ViewModelNavigationBase
{
public LoginViewModel()
{
LoginCommand = new RelayCommand(Login);
}
private void Login(object param)
{
// Just there to make sure the ICommand is actually called when I press the
// Login button
Window popup = new Window();
popup.ShowDialog();
}
public String Username { get; set; }
public String Password { get; set; }
public ICommand LoginCommand { get; set; }
}
ViewModelNavigationBase is just a class that implements the INotifyPropertyChanged interface, and Test.xaml and TestViewModel.cs are just a dummy viewmodel/view for test purposes.
In my answer, I stated that you should declare your view model DataTemplates in App.xaml so that every view will have access to them. Putting them in the MainWindow class is your first problem.
Another mistake is your Binding Path for your ICommand. If you want to access something from the view model that is set as the Window.DataContext, then you should not use a RelativeSource Binding . Try this instead:
<Button Content="Create new user" Grid.Column="3" Grid.Row="3" Margin="5" Padding="5"
Command="{Binding DataContext.DisplayTest}, Mode=OneWay}" />
Also remember that for whatever reason, you chose not to make your MainViewModel class extend the ViewModelNavigationBase class... that could also cause you problems.
Anyway, if that doesn't sort out your problems, just let me know. Also, if you want to notify a user at anytime on Stack Overflow, just put an # symbol in front of their name and they will receive a notification. You could have asked me this question directly if you had done that.
MainViewModel is not a direct ancestor in the visual or logical tree, which is why RelativeSource={RelativeSource AncestorType={x:Type vm:MainViewModel}} cannot find it.
How do you fix it? First, please don't try and reach through various UI components like this to trigger commands. Just because you saw it somewhere on the internet doesn't mean it is a desirable design choice. Doing this means the LoginView has a deep understanding of other views and view models - which is bad. If you are going to do that then you might as well code everything as one single UI class with a single viewmodel that is really just a massive code behind class.
A better (but still not optimal) approach is to have the MainView (or viewmodel) spawn the LoginView. As it holds the reference to the view, it is also responsible for disposing of it. So the LoginView can be shown to collect credentials, then the main view can dispose if it signals that the credentials are validated successfully. Or it can just collect credentials and leave it up to the MainView/viewmodel to validate them (which can be done by the MainView/viewmodel triggering a background call to check the credentials against a store).
A simple (crude) rule of thumb is: a parent view can know about a child view, but in general the reverse should not happen. MVVM is about decoupling and segregating functionality, but instead you are tightly coupling them. Of course all this gets a whole lot more complex than what I've illustrated, but you can still do some of this while keeping it practical and not over-engineering.
So, TLDR;:
the LoginView (or its viewmodel) should implement its own command to deal with the button click
don't reach deep through the entrails of another view to trigger functionality
strive for SRP and de-coupled code/views
when using ancestor binding, look for something that's in the visual/logical tree
Define MainViewModel in App scope as a static resource.
<App.Resources>
<MainViewModel x:Key="MainViewModel" />
</App.Resources>
Then you will be able to bind MainViewModel commands from any view.
<Button Command="{Binding Source={StaticResource MainViewModel}, Path=DisplayTest}" />
EDIT
Or try this code:
<Button Command="{Binding DisplayTest, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window), Path=DataContext}}"/>
Related
I tried to create a simple example of my problem:
Lets say we have the following UserControl with a Label, Image, region and a Button:
<UserControl x:Class="SR.Soykaf.Client.Main.Simple.SimpleTabView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:core="clr-namespace:SR.Soykaf.Client.Core.Core;assembly=SR.Soykaf.Client.Core"
xmlns:regions="http://prismlibrary.com/"
mc:Ignorable="d"
regions:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" HorizontalAlignment="Center" Content="TITLE" />
<Image Grid.Row="1" Stretch="Uniform" UseLayoutRounding="True" Height="200" Width="153" Source="{Binding PortraitImage}" />
<ContentControl Grid.Row="2" regions:RegionManager.RegionName="{x:Static core:RegionNames.CombatWizardRegion}" />
<Button Grid.Row="3" Content="Change view in region" Command="{Binding ChangeViewCommand}" />
</Grid>
</Grid>
</UserControl>
With this ViewModel:
public class SimpleTabViewModel : BaseViewModel
{
private int _state;
private BitmapImage _portraitImage;
public BitmapImage PortraitImage
{
get => _portraitImage;
set => SetProperty(ref _portraitImage, value);
}
public ICommand ChangeViewCommand { get; set; }
public SimpleTabViewModel()
{
SetPortraitImage();
_state = 0;
ChangeViewCommand = new DelegateCommand(ChangeView);
}
private void ChangeView()
{
if (_state == 0)
{
RegionManager.RequestNavigate(RegionNames.CombatWizardRegion, new Uri(nameof(WeaponWizardView), UriKind.Relative));
_state = 1;
}
else
{
RegionManager.RequestNavigate(RegionNames.CombatWizardRegion, new Uri(nameof(SpellWizardView), UriKind.Relative));
_state = 0;
}
}
private void SetPortraitImage()
{
PortraitImage = new BitmapImage();
var resource = typeof(Data.Characters.PlayerCharacter).Assembly.GetManifestResourceNames().Single(x => x.ContainsIgnoreCase("PC_Hawk"));
using (var stream = typeof(Data.Characters.PlayerCharacter).Assembly.GetManifestResourceStream(resource))
{
PortraitImage.BeginInit();
PortraitImage.StreamSource = stream;
PortraitImage.CacheOption = BitmapCacheOption.OnLoad;
PortraitImage.EndInit();
}
}
}
As you can see, the Button will switch between 2 views for the CombatWizardRegion region. These are the simple views:
View 1:
<UserControl x:Class="SR.Soykaf.Client.Main.Simple.SpellWizardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mvvm="http://prismlibrary.com/"
mc:Ignorable="d"
mvvm:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0" HorizontalAlignment="Center" Content="Spell" />
<Image Grid.Row="1" Stretch="Uniform" UseLayoutRounding="True" Height="200" Width="153" Source="{Binding DataContext.PortraitImage, RelativeSource={RelativeSource AncestorType={x:Type UserControl}, AncestorLevel=2}}" />
</Grid>
</UserControl>
View 2:
<UserControl x:Class="SR.Soykaf.Client.Main.Simple.WeaponWizardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mvvm="http://prismlibrary.com/"
mc:Ignorable="d"
mvvm:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0" HorizontalAlignment="Center" Content="Weapon" />
<Image Grid.Row="1" Stretch="Uniform" UseLayoutRounding="True" Height="200" Width="153" Source="{Binding DataContext.PortraitImage, RelativeSource={RelativeSource AncestorType={x:Type UserControl}, AncestorLevel=2}}" />
</Grid>
</UserControl>
These two views basically bind their image source to the PortraitImage property of the parent view model. Both views also have a unique label: Spell and Weapon to distinguish between the two of them.
Then I register the 2 views:
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(SpellWizardView));
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(WeaponWizardView));
Launching this application works fine - on first sight:
But... then we click the button to switch views:
Why is the image not loading in the second view? It has the same binding code as the first view.
(Also if I first register the WeaponCombatView to the region, then that view works but the SpellCombatView doesn't work anymore.)
I get this error for the view which is registered last:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.UserControl', AncestorLevel='2''. BindingExpression:Path=DataContext.PortraitImage; DataItem=null; target element is 'Image' (Name=''); target property is 'Source' (type 'ImageSource')
Also interesting : During debugging if I change the AncesterLevel to 3 and back to 2, the image suddenly appears because the binding seems to get refreshed. I also checked the Visual Tree and I don't see any problems.
Thanks in advance!
The RegisterViewWithRegion method enables view discovery, but is intended to construct and show the specified view when the control is loaded. The navigation service allows for changing the view dynamically. Typically, you would use RegisterViewWithRegion for static views like a menu, that do not change.
[..] if I first register the WeaponCombatView to the region, then that view works but the SpellCombatView doesn't work anymore.
Your CombatWizardRegion is in a ContentControl which uses a SingleActiveRegion in the default Prism region adapter. This means, that it can only display a single active view at once in it. When using RegisterViewWithRegion to register multiple views, they will be registered and both will be added to the Views collection of the region, but only the first one will be added to the ActiveViews collection and displayed. For your registrations below, effectively only the first one will be displayed.
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(SpellWizardView));
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(WeaponWizardView));
After navigation your views do not display, because you did not register them for navigation in the container, so the navigation service will not find them. The region manager will store them different internally with RegisterViewWithRegion, that is why you cannot just register them that way. Instead register them like this in Prism >=7:
containerRegistry.RegisterForNavigation<SpellWizardView>();
containerRegistry.RegisterForNavigation<WeaponWizardView>();
For older versions of Prism <=6 you have to use one of these methods:
containerRegistry.RegisterTypeForNavigation<SpellWizardView>(nameof(SpellWizardView));
containerRegistry.Register<object, SpellWizardView>(nameof(SpellWizardView));
containerRegistry.Register(typeof(object), typeof(SpellWizardView), nameof(SpellWizardView));
I recommend you to use either RegisterViewWithRegion for static regions or the navigation service for dynamic regions, where you need to change views with the navigation service. You can navigate to the initial view instead of registering it with the region manager and RegisterViewWithRegion.
I get this error for the view which is registered last: System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.UserControl', AncestorLevel='2''.
This binding works fine, it is failing because of the single active region issue above. Both views get added to the Views collection of the CombatWizardRegion, but only the first is added to the ActiveViews collection and displayed and therefore set as Content of the corresponding ContentControl. Consequently, the first view is in the visual tree and receives the data context, while the second view is not in the visual tree, so its data context is null and there is no ancestor, which causes the error.
I have a problem with binding a button located in a sidebar in my windows phone app. It seems like the buttons binding just dissapears..
Here's my code at the moment
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<sidebar:SidebarControl x:Name="sidebarControl"
HeaderText="WP"
HeaderBackground="YellowGreen"
HeaderForeground="White"
SidebarBackground="{StaticResource PhoneChromeBrush}">
<sidebar:SidebarControl.SidebarContent>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width="380">
<Button Content="Go to page 2" x:Name="GoToPage2"/>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
</Grid>
</sidebar:SidebarControl.SidebarContent>
<Grid VerticalAlignment="Top" HorizontalAlignment="Stretch"
Margin="12">
<TextBlock Style="{StaticResource PhoneTextNormalStyle}">Your current view goes here</TextBlock>
</Grid>
</sidebar:SidebarControl>
</Grid>
</Grid>
At the moment I am using a nuget for the sidebar called SidebarWP8. Maybe Calinbrun.Micro doesnt work with this? Or do I have to insert a binding to the VM in a grid?
Here's the method in the ViewModel:
private readonly INavigationService navigationService;
public MainPageViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
}
public void GoToPage2()
{
navigationService.UriFor<Page2ViewModel>().Navigate();
}
<Button cm:Message.Attach="[Event Click] = [Action GoToPage2()]" />
This should work, because of the other commenter is correct with respect to the default controls... Custom controls require adding some more handling in which can be a pain in the butt but doing it with the short hand above CM will look for that property in the button and process accordingly.
I checked the source for Caliburn Micro: I handles the convention binding by traversing the visual tree to find elements with a name. But it only checks the default Content property for each control.
That means it will only go through the Content or Children elements and not Custom properties like SidebarContent, so named elements will not be found there.
You have to wire it up by hand (by binding a command or adding a click handler).
I have my app up and running with CM. The shell view contains multiple ContentControls which itself contain views. Those are assigned by convention as they got a name matching a property in the view model.
I got a WPF window (at least its class inherits from Window) which is opened from my app with windowManager.ShowDialog(viewModel). Inside this window again I have ContentControls but those do not bind to view model properties.
I already tried to set cal:View.ApplyConventions="True" on the window but that did not help either. I also used ViewModelBinder.Bind(viewmodel, view, null) to bind vm and view before showing the window - does not work either.
How can I make sure my opened window (modal dialog) makes use of the same CM enhancements?
This is the view:
<Window x:Class="Client.Views.History.HistoryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
Title="Historie" Height="300" Width="300"
cal:View.ApplyConventions="True"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="64" />
<RowDefinition Height="*" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<ContentControl x:Name="HeaderView" Grid.Row="0" Grid.Column="0" />
<ContentControl x:Name="RecordView" Grid.Row="1" Grid.Column="0" />
<Border Grid.Row="2" Grid.Column="0" Background="DarkKhaki" BorderThickness="2" BorderBrush="DarkSeaGreen" />
</Grid>
</Window>
The border is displayed so the view is loaded. The DataContext should be set by caliburn micro. This is the view model:
public class HistoryViewModel : PropertyChangedBase
{
#region Fields --------------------------------------------------------
private readonly HeaderViewModel headerView;
private readonly RecordViewModel recordView;
#endregion
public HistoryViewModel()
{
this.headerView = IoC.Get<HeaderViewModel>();
this.recordView = IoC.Get<RecordViewModel>();
}
public HeaderViewModel HeaderView
{
get { return this.headerView; }
}
public RecordViewModel RecordView
{
get { return this.recordView; }
}
}
The constructor runs, the view models are created (not null). But the properties HeaderView and RecordView are never accessed.
To launch this I use this code:
HistoryViewModel viewModel = IoC.Get<HistoryViewModel>();
windowManager.ShowDialog(viewModel);
It works when binding the model explicitly like this:
<ContentControl x:Name="HeaderView" cal:View.Model="{Binding HeaderView}" Grid.Row="0" Grid.Column="0" />
<ContentControl x:Name="RecordView" cal:View.Model="{Binding RecordView}" Grid.Row="1" Grid.Column="0" />
But I would like to know how the standard mechaniscs can be re-enabled.
I think your HistoryViewModel needs to be a Conductor to get this to work. Since you want both child VMs to show at the same time, you would inherit from Conductor.Collection.AllActive.
John
I'm new to WPF, but have been able make a lot of progress in short time thanks to a good book on the topic, and of course, quality posts on sites like this one. However, now I've come across something I can seem to figure out by those means, so I posting my first question.
I've have a ControlTemplate in a resource dictionary which I apply to several UserControl views. The template provides a simple overlay border and two buttons: Save and Cancel. The templated user control holds various text boxes, etc., and is bound to some ViewModel depending on the context. I'm trying to figure out how to bind the commands to the Save/Cancel buttons when I use/declare the UserControl in some view. Is this is even possible, or am I doing something very wrong?
First, the template:
<ControlTemplate x:Key="OverlayEditorDialog"
TargetType="ContentControl">
<Grid>
<Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="DarkGray"
Opacity=".7"/>
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="DarkGray">
<Grid>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ContentPresenter Grid.Row="0"/>
<Grid Grid.Row="1"
Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1"
Content="Cancel"
***Command="{Binding CancelCommand}}"**
/>
<Button Grid.Column="0"
Content="Save"
***Command="{Binding Path=SaveCommand}"***/>
</Grid>
</Grid>
</Border>
</Grid>
</ControlTemplate>
The template in turn is used in the CustomerEditorOverlay user control
<UserControl x:Class="GarazhApp.View.CustomerEditorOverlay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
<UserControl.Resources>
<ResourceDictionary Source="Dictionary1.xaml"/>
</UserControl.Resources>
<ContentControl Template="{StaticResource ResourceKey=OverlayEditorDialog}">
<Grid Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<SomeElement/>
<SomeOtherElement/>
</Grid>
</ContentControl>
...and finally, the user control is used as part of a view like so:
<local:CustomerEditorOverlay Visibility="{Binding Path=CustomerViewModel.ViewMode, Converter={StaticResource myConverter}, FallbackValue=Collapsed}"
d:IsHidden="True" />
So, based on what I've learned from a project I have been on forever and a half, we have a workable pattern.
Let's say you have a bunch of modal windows that all get applied the same style within the application. To have Save and Cancel buttons on each view, the UserControl used for all of the modal windows has several dependency properties. In addition, we specify virtual methods for your commands (e.g. OnSaveCommand, OnCancelCommand, CanExecuteSaveCommand, CanExecuteCancelCommand) and the commands themselves as properties in a base ViewModel that is inherited by your views.
Ultimately, what happens is we create new modal windows by simply doing this:
<my:YourBaseView x:class="MyFirstView" xmlns:whatever="whatever" [...]>
<my:YourBaseView.PrimaryButton>
<Button Content="Save" Command="{Binding SaveCommand}" />
</my:YourBaseView.PrimaryButton>
<!-- some content -->
</my:YourBaseView>
With accompanying code-behind:
public class MyFirstView : YourBaseView
{
[Import] /* using MEF, but you can also do MvvmLight or whatever */
public MyFirstViewModel ViewModel { /* based on datacontext */ }
}
And a ViewModel:
public class MyFirstViewModel : ViewModelBase
{
public override OnSaveCommand(object commandParameter)
{
/* do something on save */
}
}
The template for this UserControl specifies ContentControls in a grid layout with the Content property bound to the PrimaryButton and SecondaryButton. Of course, the content for the modal is stored in the Content property of the UserControl and displayed in a ContentPresenter as well.
<Style TargetType="{x:Type my:YourBaseView}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type my:YourBaseView}">
<Grid>
<!-- ignoring layout stuff -->
<ContentControl Content="{TemplateBinding Content}" />
<ContentControl Content="{TemplateBinding PrimaryButton}" />
<ContentControl Content="{TemplateBinding SecondaryButton}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
UserControl code:
public class YourBaseView : UserControl
{
public static readonly DependencyProperty PrimaryButtonProperty =
DependencyProperty.Register("PrimaryButton", typeof(Button), typeof(YourBaseView), new PropertyMetadata(null));
public Button PrimaryButton
{
get { return (Button)GetValue(PrimaryButtonProperty); }
set { SetValue(PrimaryButtonProperty, value); }
}
/* and so on */
}
You can change the style for each instance of your templated view, of course. We just happen to stick with one base style.
TL;DR edit: I may have gone a bit overboard since I think you just need the understanding that exposing dependency properties of type Button which are set up through the XAML each time you create a new overlay. That, or you could probably RelativeSource your way back up to the visual tree with something like {Binding DataContext.SaveCommand, RelativeSource={RelativeSource AncestorType={x:Type MyView}}} but it's a little dirtier.
I am currently binding an object in my code-behind (C#) to my XAML by giving a name to the XAML control, and setting the DataContext in the code-behind.
public partial class SmsControl: UserControl
{
private readonly DataOrganizer _dataOrganizer;
public FunctionalTester _funcTester;
public SmsControl()
{
InitializeComponent();
_dataOrganizer = new DataOrganizer();
_funcTester = new FunctionalTester();
// Set the datacontext appropriately
grpModemInitialization.DataContext = _funcTester;
}
private async void Button_Click_2(object sender, RoutedEventArgs e)
{
await _funcTester.Test();
}
}
And my XAML...
<!-- SMS Test Modem Initialization GroupBox -->
<GroupBox x:Name="grpModemInitialization" Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="3" Style="{StaticResource groupboxViewItem}">
<GroupBox.Header>
<Label Content="SMS Test Modem Initialization" Style="{StaticResource boldHeaderItem}"/>
</GroupBox.Header>
<!-- SMS Test Modem Initialization Grid -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Content="COM:" Style="{StaticResource boldHeaderItem}" />
<ComboBox Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" Style="{StaticResource comboBoxItem}" ItemsSource="{Binding AvailableCommPorts}" SelectedItem="{Binding SelectedCommPort}" />
<Label Grid.Column="2" Grid.Row="0" Content="Modem Ready:" Style="{StaticResource boldHeaderItem}" />
<Label Grid.Column="2" Grid.Row="1" Content="RSSI:" Style="{StaticResource boldHeaderItem}" />
<Label Content="{Binding ModemReady}" Grid.Column="3" Grid.Row="0" HorizontalContentAlignment="Left" VerticalAlignment="Center"/>
<Label Content="{Binding ModemRssi}" Grid.Column="3" Grid.Row="1" HorizontalContentAlignment="Left" VerticalAlignment="Center" />
<Label Grid.Column="4" Grid.Row="0" Content="Modem #:" Style="{StaticResource boldHeaderItem}"/>
<Button Grid.Column="4" Grid.Row="1" Grid.ColumnSpan="2" Content="Initialize" />
<Label Content="{Binding ModemNumber}" Grid.Column="5" Grid.Row="0" HorizontalContentAlignment="Left" VerticalAlignment="Center"/>
</Grid>
</GroupBox>
The code above works fine - no problems. What I'm asking is, if there is a way to set the DataContext of the GroupBox in XAML, referencing my _funcTester object, instead of setting the DataContext in the code-behind? The reason I ask, is because different controls need to be bound to different objects in the code-behind and I'm not finding good resources on how to do so, except as I show above (giving a "x:Name" to each XAML control and setting the DataContext in code-behind). Any help is appreciated! Thanks!
You don't want to reference UI elements by name in the code-behind. Actually any time you can avoid naming an object you save a little in performance. And by setting up your app to use MVVM properly, you gain in performance, readability, maintainability, and code separation.
You want to abstract things further to use the MVVM pattern. You're doing your bindings correctly but consider the pattern. Your view is all correct. Consider adding a class that holds the properties defined currently in your code-behind and the methods called in your event handlers.
public class ViewModel : INotifyPropertyChanged
{
private FunctionalTester _funcTester;
public FunctionalTester FuncTester
{
get
{
return _funcTester;
}
set
{
_funcTester = value;
OnPropertyChanged( "FuncTester" );
}
}
public async void TestAsync( )
{
await _funcTester.Test( );
}
}
A binding to the FuncTester would simply be SomeProperty="{Binding FuncTester}" because the object is set as the DataContext of your view. A decent article that expands on this idea is found here.
Obviously left out some things (like INotifyPropertyChanged implementation and other properties you've defined) for brevity. But just make this class and assign it as your view model. Then the UI (the Xaml and the code-behind) only really deal with the UI and the ViewModel really deals with the data and logic. Great separation. For your event handler, just call ((ViewModel)this.DataContext).Test( ); and you can plug-and-play your ViewModel's to change functionality on the fly. Hope this helps!
Just set the DataContext of whole UserControl to self i.e do
this.DataContext = this; in constructor.
Then define the Property for _functinTester
public FunctionalTester FuncTester { get {return _funcTester} };
Now in your xaml you can do
<GroupBox x:Name="grpModemInitialization" DataContext="{Binding FuncTester}"/>
In this way since you have the DataContext set for your whole usercontrol, you can bind any control to any property within that DataContext