I'm trying to grasp the new compiled bindings, but right at the start I get stopped by this simple problem.
I have Hub control with one HubSection. The content of this section is an ItemsControl that needs to bind to view models' observable collection. I can't get this binding to work as I expect it to.
<Pivot x:Name="rootPivot" Style="{StaticResource TabsStylePivotStyle}">
<PivotItem>
<Hub>
<HubSection Header="News">
<DataTemplate x:DataType="local:HomePage">
<ItemsControl ItemsSource="{x:Bind ViewModel.NewsItems, Mode=OneWay}" />
ViewModel property is just a property and is instantiated before InitializeComponents() call. NewsItems is observable collection inside view model that is filled after the page has loaded - asynchronously (web request).
What am I doing wrong here?
EDIT: Code-behind
HomePage.xaml.cs
/// <summary>
/// Home pag view.
/// </summary>
public sealed partial class HomePage : Page
{
/// <summary>
/// Initializes a new instance of the <see cref="HomePage"/> class.
/// </summary>
public HomePage()
{
// Retrieve view model
this.ViewModel = ViewModelResolver.Home;
// Trigger view model loaded on page loaded
this.Loaded += (sender, args) => this.ViewModel.LoadedAsync();
this.InitializeComponent();
}
/// <summary>
/// Gets the view model.
/// </summary>
/// <value>
/// The view model.
/// </value>
public IHomeViewModel ViewModel { get; }
}
HomePageViewModel.cs
/// <summary>
/// Home view model.
/// </summary>
public sealed class HomeViewModel : IHomeViewModel
{
/// <summary>
/// Occurs on page loaded.
/// </summary>
public async Task LoadedAsync()
{
// Retrieve news items
var news = await new NewsService().GetNewsAsync();
foreach (var newsItem in news)
this.NewsItems.Add(newsItem);
}
/// <summary>
/// Gets the news items.
/// </summary>
/// <value>
/// The news items.
/// </value>
public ObservableCollection<IFeedItem> NewsItems { get; } = new ObservableCollection<IFeedItem>();
}
This is indeed an interesting question. I guess the issue is that, unlike typical DataTemplate like the following (see its parent ListView is binding to some known data Model.Items)
<ListView ItemsSource="{x:Bind Model.Items}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Item">
<Grid>
<TextBlock Text="{x:Bind Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
your top level DataTemplate however, doesn't know where the data comes from.
So the fix is to tell the HubSection to bind the right data - in this case, the HomePage.xaml.cs instance. So, try adding this to your Hub
<Hub DataContext="{x:Bind}">
Or simply add
this.InitializeComponent();
this.DataContext = this;
Either way should fix your issue.
Related
I am trying to populate a listbox with a bunch of serial data. I am saving the data to an ObservableCollection named SerialData (SerialData is type SerialMessage, a class that contains a bunch of data including 2 strings, time stamp and Message that I want to display). The ObservableCollection is a member of the DataCollector class. In the MainWindowViewModel I have declared the DataCollector and made it public. In my MainWindow the datacontext is set to MainWindowViewModel.
I want to bind the SerialData to a listbox on the MainWindow where I display the timestamp and message. I have tried several methods of binding the path to the SerialData but it does not show up in the listbox. I have confirmed that SerialData is updated correctly.
Is it possible to bind to the property of a property and display it's members?
My Code snippets:
SerialMessage class
...
/// <summary>
/// The time the message was received
/// </summary>
public DateTime TimeRecived
{
get;
set;
}
/// <summary>
/// The message on the serial bus
/// </summary>
public String Message
{
get;
set;
}
...
Mainwindow
...
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
...
<ListView Grid.Column="0" Grid.Row="1" Name="MessageList" Margin="10" ItemsSource="{Binding Path=DC.SerialData}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding TimeRecived, StringFormat=dd-MM-yyyy HH:mm:ss.fff}"></TextBlock>
<TextBlock Text="{Binding Message}" Margin="10,0,0,0"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListView>
...
MainWindowViewModel
...
/// <summary>
/// Instance of data collector
/// </summary>
public DataCollector DC
{
get
{
return m_dataCollector;
}
}
...
DataCollector Class
...
/// <summary>
/// All of the Data recived on the serial Bus as it comes in
/// </summary>
public ObservableCollection<SerialMessage> SerialData
{
get;
private set;
}
...
There could be several reasons for not seeing the collection data.
One possibility is a missing notification.
If you assign a value to the SerialData collection after the initial XAML loading routines have finished, your view won't notice that you've set SerialData later on.
You need to raise INotifyPropertyChanged.PropertyChanged in the setter to notify the view of the property change.
class DataCollector : INotifyPropertyChanged {
...
public event PropertyChangedEventHandler PropertyChanged;
...
private ObservableCollection<SerialMessage> _serialData;
....
//somewhere some line code assigns to SerialData
SerialData = GetSerialData();
/// <summary>
/// All of the Data recived on the serial Bus as it comes in
/// </summary>
public ObservableCollection<SerialMessage> SerialData
{
get => _serialData;
private set {
if (value != _serialData)
{
_serialData = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventsArgs(nameof (SerialData)));
}
}
I have to validate some Properties, before I try to save my Entity into Database.
Problem:
Programmstart: Validation error is shown of textbox in tabitem1
User select: Tabitem2 in the View
User select: Tabitem1 in the View
Validation error of empty textbox is not shown in tabitem1 anymore.
Excepted behavior:
Validation error shall be shown every time also if the user changes the selected tabitem.
Tools / Frameworks used:
Prism 6.3 (New Project with Templatepack PrismUnity
Prism.Validation (https://github.com/mfe-/Prism.Validation)
Questions:
Why the DataAnnoation is not shown anymore after selection between the different tabitems? The ViewModel property hasErrors is true.
How I can restart the evaluation, if the user selected tabitem1 again?
View:
<Window x:Class="PrismUnityApp1TestValidation.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Title="{Binding Title}" Height="350" Width="525">
<StackPanel>
<!--<ContentControl prism:RegionManager.RegionName="ContentRegion" />-->
<TabControl>
<TabItem>
<TabItem.Content>
<TextBox Height="50" Text="{Binding TestText, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></TextBox>
</TabItem.Content>
</TabItem>
<TabItem>
<TabItem.Content>
<TextBlock Text="TabItem2"></TextBlock>
</TabItem.Content>
</TabItem>
</TabControl>
</StackPanel>
</Window>
ViewModel:
using System.ComponentModel.DataAnnotations;
using Prism.Mvvm;
using Prism.Validation;
namespace PrismUnityApp1TestValidation.ViewModels
{
public class MainWindowViewModel : ValidatableBindableBase
{
private string _title = "Prism Unity Application";
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
private string _testtext;
[Required]
public string TestText
{
get { return _testtext; }
set { SetProperty(ref _testtext, value); }
}
public MainWindowViewModel()
{
}
}
}
ValidatableBindableBase (NugetPackage Prism.Validation):
namespace Prism.Validation
{
/// <summary>
/// The IValidatableBindableBase interface was created to add validation support for model classes that contain validation rules.
/// The default implementation of IValidatableBindableBase is the ValidatableBindableBase class, which contains the logic to run the validation rules of the
/// instance of a model class and return the results of this validation as a list of properties' errors.
/// </summary>
// Documentation on validating user input is at http://go.microsoft.com/fwlink/?LinkID=288817&clcid=0x409
public class ValidatableBindableBase : BindableBase, IValidatableBindableBase, INotifyDataErrorInfo
{
private readonly BindableValidator _bindableValidator;
/// <summary>
/// Initializes a new instance of the <see cref="ValidatableBindableBase"/> class.
/// </summary>
public ValidatableBindableBase()
{
_bindableValidator = new BindableValidator(this);
}
/// <summary>
/// Gets or sets a value indicating whether this instance is validation enabled.
/// </summary>
/// <value>
/// <c>true</c> if validation is enabled for this instance; otherwise, <c>false</c>.
/// </value>
public bool IsValidationEnabled
{
get { return _bindableValidator.IsValidationEnabled; }
set { _bindableValidator.IsValidationEnabled = value; }
}
/// <summary>
/// Returns the BindableValidator instance that has an indexer property.
/// </summary>
/// <value>
/// The Bindable Validator Indexer property.
/// </value>
public BindableValidator Errors
{
get
{
return _bindableValidator;
}
}
/// <summary>
/// Gets a value that indicates whether the entity has validation errors.
/// </summary>
/// <value>
/// <c>true</c> if this instance contains validation errors; otherwise, <c>false</c>.
/// </value>
public bool HasErrors
{
get
{
return !ValidateProperties();
}
}
/// <summary>
/// Occurs when the Errors collection changed because new errors were added or old errors were fixed.
/// </summary>
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged
{
add { _bindableValidator.ErrorsChanged += value; }
remove { _bindableValidator.ErrorsChanged -= value; }
}
/// <summary>
/// Gets all errors.
/// </summary>
/// <returns> A ReadOnlyDictionary that's key is a property name and the value is a ReadOnlyCollection of the error strings.</returns>
public ReadOnlyDictionary<string, ReadOnlyCollection<string>> GetAllErrors()
{
return _bindableValidator.GetAllErrors();
}
/// <summary>
/// Validates the properties of the current instance.
/// </summary>
/// <returns>
/// Returns <c>true</c> if all properties pass the validation rules; otherwise, false.
/// </returns>
public bool ValidateProperties()
{
return _bindableValidator.ValidateProperties();
}
/// <summary>
/// Validates a single property with the given name of the current instance.
/// </summary>
/// <param name="propertyName">The property to be validated.</param>
/// <returns>Returns <c>true</c> if the property passes the validation rules; otherwise, false.</returns>
public bool ValidateProperty(string propertyName)
{
return !_bindableValidator.IsValidationEnabled // don't fail if validation is disabled
|| _bindableValidator.ValidateProperty(propertyName);
}
/// <summary>
/// Sets the error collection of this instance.
/// </summary>
/// <param name="entityErrors">The entity errors.</param>
public void SetAllErrors(IDictionary<string, ReadOnlyCollection<string>> entityErrors)
{
_bindableValidator.SetAllErrors(entityErrors);
}
/// <summary>
/// Checks if a property already matches a desired value. Sets the property and
/// notifies listeners only when necessary. We are overriding this property to ensure that the SetProperty and the ValidateProperty methods are fired in a
/// deterministic way.
/// </summary>
/// <typeparam name="T">Type of the property.</typeparam>
/// <param name="storage">Reference to a property with both getter and setter.</param>
/// <param name="value">Desired value for the property.</param>
/// <param name="propertyName">Name of the property used to notify listeners. This
/// value is optional and can be provided automatically when invoked from compilers that
/// support CallerMemberName.</param>
/// <returns>
/// True if the value was changed, false if the existing value matched the
/// desired value.
/// </returns>
protected override bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
var result = base.SetProperty(ref storage, value, propertyName);
if (result && !string.IsNullOrEmpty(propertyName))
{
if (_bindableValidator.IsValidationEnabled)
{
_bindableValidator.ValidateProperty(propertyName);
}
}
return result;
}
/// <summary>
/// Gets the validation errors for a specified property or for the entire entity.
/// </summary>
/// <param name="propertyName">The name of the property to retrieve validation errors for; or null or Empty, to retrieve entity-level errors.</param>
/// <returns>The validation errors for the property or entity.</returns>
public IEnumerable GetErrors(string propertyName)
{
if (HasErrors==false)
{
return Enumerable.Empty<String>();
}
return _bindableValidator[propertyName];
}
}
}
I found a solution here:
https://social.msdn.microsoft.com/Forums/vstudio/en-US/fb50537c-feec-42dc-8439-dcf78ef8951a/validation-error-and-tab-control?forum=wpf
I changed as mentioned in the post my View.
View:
<Window x:Class="PrismUnityApp1TestValidation.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Title="{Binding Title}" Height="350" Width="525">
<StackPanel>
<!--<ContentControl prism:RegionManager.RegionName="ContentRegion" />-->
<TabControl>
<TabItem IsSelected="{Binding TabItem1Selected}">
<TabItem.Content>
<AdornerDecorator>
<TextBox Height="50" Text="{Binding TestText, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></TextBox>
</AdornerDecorator>
</TabItem.Content>
</TabItem>
<TabItem>
<TabItem.Content>
<TextBlock Text="TabItem2"></TextBlock>
</TabItem.Content>
</TabItem>
</TabControl>
</StackPanel>
</Window>
In MS VS 2015 Professional I develop C# WPF MVVM application using Catel as MVVM framework. My problem is I don't know how to realize switching among multiple views in one window using buttons. Below I briefly describe my application. The MainWindow has three buttons
<catel:Window x:Class="FlowmeterConfigurator.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catel="http://catel.codeplex.com"
ResizeMode="CanResize">
<catel:StackGrid x:Name="LayoutRoot">
<catel:StackGrid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
</catel:StackGrid.RowDefinitions>
<ToolBar>
<Button Name="btnConnectDisconnect" Content="Connect/Disconnect"/>
<Button Name="btnFieldSettings" Content="Field Settings"/>
<Button Name="btnCalibration" Content="Flowmeter Calibration"/>
</ToolBar>
</catel:StackGrid>
</catel:Window>
Application MainWindow has a ViewModel. For brevity I don't show it here. In addition to MainWindow there are three views in my application: ConnectDisconnectView, CalibrationView and FieldSettingsView. For brevity I show here only one of them (FieldSettingsView) because all of others are created in the same manner on the base of catel:UserControl.
<catel:UserControl x:Class="FlowmeterConfigurator.Views.FieldSettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catel="http://catel.codeplex.com">
<catel:StackGrid>
<catel:StackGrid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</catel:StackGrid.RowDefinitions>
<catel:StackGrid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</catel:StackGrid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Flowmeter Serial Number"/>
<TextBox Name="SerialNumber" Grid.Row="0" Grid.Column="1"/>
</catel:StackGrid>
</catel:UserControl>
Each of these views has a Model. I show here only one of these Models because all of them created in the same manner.
using Catel.Data;
namespace FlowmeterConfigurator.Models
{
/// <summary>
/// Field Settings Model.
/// </summary>
public class FieldSettingsModel : SavableModelBase<FieldSettingsModel>
{
/// <summary>
/// Returns flowmeter serial number.
/// </summary>
public string SerialNumber
{
get { return GetValue<string>(SerialNumberProperty); }
set { SetValue(SerialNumberProperty, value); }
}
/// <summary>
/// Register SerialNumber property.
/// </summary>
public static readonly PropertyData SerialNumberProperty = RegisterProperty("SerialNumber", typeof(string), null);
}
}
Each of these views has a ViewModel. I show here only one of these ViewModels because all of them created in the same manner.
using Catel;
using Catel.Data;
using Catel.MVVM;
using FlowmeterConfigurator.Models;
namespace FlowmeterConfigurator.ViewModels
{
/// <summary>
/// Field settings ViewModel.
/// </summary>
public class FieldSettingsViewModel : ViewModelBase
{
/// <summary>
/// Creates a FieldSettingsViewModel instance.
/// </summary>
/// <param name="fieldSettingsModel">Field settings Model.</param>
public FieldSettingsViewModel(FieldSettingsModel fieldSettingsModel)
{
Argument.IsNotNull(() => fieldSettingsModel);
FieldSettings = fieldSettingsModel;
}
/// <summary>
/// Returns or sets Field Settings Model.
/// </summary>
[Model]
public FieldSettingsModel FieldSettings
{
get { return GetValue<FieldSettingsModel>(FieldSettingsProperty); }
set { SetValue(FieldSettingsProperty, value); }
}
/// <summary>
/// Here I register FieldSettings property.
/// </summary>
public static readonly PropertyData FieldSettingsProperty = RegisterProperty("FieldSettings", typeof(FieldSettingsModel), null);
/// <summary>
/// Returns or sets flowmeter serial number.
/// </summary>
[ViewModelToModel("FieldSettings")]
public string SerialNumber
{
get { return GetValue<string>(SerialNumberProperty); }
set { SetValue(SerialNumberProperty, value); }
}
/// <summary>
/// Here I register SerialNumber property.
/// </summary>
public static readonly PropertyData SerialNumberProperty = RegisterProperty("SerialNumber", typeof(string), null);
}
}
Directly after my application loading, ConnectDisconnectView must be displayed. And then user can switch the views at will using the buttons on MainWindow toolbar. The switching among the Views must be in the following manner: if (for example) the current displayed view is "ConnectDisconnectView" and user presses "Field Settings" button then "ConnectDisconnectView" view must disappear from MainWindow and "FieldSettingsView" view must appear and must be displayed in MainWindow. And so on. That is when pressed appropriate button in MainWindow toolbar (for example "Flowmeter Calibration") the appropriate view (CalibrationView) must be displayed in MainWindow and other views must not be displayed. How can I realize this capability in my application? Your help will be appreciate highly.
P.S. Of course as you see the number and content of Views are reduced here for brevity and clarity. In real world the number of Views in my application is about 20 - 25 and they must contain complex graphics and table information.
First I show you xaml code:
<catel:Window.Resources>
<catel:ViewModelToViewConverter x:Key="ViewModelToViewConverter" />
</catel:Window.Resources>
<catel:StackGrid x:Name="LayoutRoot">
<ContentControl Content="{Binding CurrentPage, Converter={StaticResource ViewModelToViewConverter}}" />
<ToolBar>
<Button Name="btnConnectDisconnect" Command={Binding Connect} Content="Connect/Disconnect"/>
<Button Name="btnFieldSettings" Command={Binding Field} Content="Field Settings"/>
<Button Name="btnCalibration" Command={Binding Calibration} Content="Flowmeter Calibration"/>
</ToolBar>
</catel:StackGrid>
Then in c# code you need this:
using Catel.Data;
using Catel.MVVM;
using System.Threading.Tasks;
public class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel()
{
this.Connect = new Command(HandleConnectCommand);
this.Field = new Command(HandleFieldCommand);
this.Calibration = new Command(HandleCalibrationCommand);
this.CurrentPage = new ConnectViewModel();
}
/// <summary>
/// Gets or sets the CurrentPage value.
/// </summary>
public IViewModel CurrentPage
{
get { return GetValue<IViewModel>(CurrentPageProperty); }
set { SetValue(CurrentPageProperty, value); }
}
/// <summary>
/// Register the CurrentPage property so it is known in the class.
/// </summary>
public static readonly PropertyData CurrentPageProperty = RegisterProperty("CurrentPage", typeof(IViewModel), null);
public Command Connect { get; private set; }
public Command Field { get; private set; }
public Command Calibration { get; private set; }
protected override async Task InitializeAsync()
{
await base.InitializeAsync();
// TODO: subscribe to events here
}
protected override async Task CloseAsync()
{
// TODO: unsubscribe from events here
await base.CloseAsync();
}
private void HandleCalibrationCommand()
{
this.CurrentPage = new CalibrationViewModel();
}
private void HandleFieldCommand()
{
this.CurrentPage = new FieldViewModel();
}
private void HandleConnectCommand()
{
this.CurrentPage = new ConnectViewModel();
}
}
When you start your application CurrentPage is to be loaded with data context ConnectViewModel(). And then with commands from buttons you can change date context for another view model.
One way to solve this problem is using regions from Prism. Catel provides an extension for Prism so you can activate view models in specific regions.
I am creating a WP8 application where I am binding contentcontrol to ViewModel. This ContentControl takes the DataTemplate specified in App.xaml.cs for that VM and bind to the contentcontrol template. But the problem is that I am not able to get the instance of that VM in my View. How can I get or pass my VM instance to my View that is been binded to the content control. Here is the code?
The problem is when DyncmicContentControl gets a ViewModel it calls GetTemplate() method to get the corresponding DataTemplate from App.xaml.cs Which creates a new instance of that View but I am not able to pass this ViewModel to that View. How can I achieve this??
ContentControl.cs
public class DynamicContentControl : ContentControl
{
/// <summary>
/// Called when the value of the <see cref="P:System.Windows.Controls.ContentControl.Content" /> property changes.
/// </summary>
/// <param name="oldContent">The old value of the <see cref="P:System.Windows.Controls.ContentControl.Content" /> property.</param>
/// <param name="newContent">The new value of the <see cref="P:System.Windows.Controls.ContentControl.Content" /> property.</param>
protected override void OnContentChanged(object oldContent, object newContent)
{
if (newContent != null)
{
base.OnContentChanged(oldContent, newContent);
this.ContentTemplate = DataTemplateSelector.GetTemplate(newContent);
}
}
}
DataTemplateSelector.cs
/// <summary>
/// Gets the template.
/// </summary>
/// <param name="param">The parameter.</param>
/// <returns></returns>
public static DataTemplate GetTemplate(object param)
{
Type t = param.GetType();
DataTemplate templateData = App.Current.Resources[t.Name] as DataTemplate;
return templateData;
}
MainPage.xaml
<Controls:DynamicContentControl Content="{Binding UsrCntrlDynamic}" />
MainPageViewModel.cs
public static ObservableCollection<object> ContentControlItems;
public MainPageViewModel()
{
ContentControlItems = new ObservableCollection<object>();
ContentControlItems.Add(new UserControlViewModel());
}
App.xaml
<DataTemplate x:Key="UserControlViewModel">
<vm:UserControlView />
</DataTemplate>
A DataTemplate set on a ContentControl's ContentTemplate property is applied to the object that is set on that ContentControl's Content property. So in this case setting the ContentTemplate should render that DataTemplate with whatever is in the UsrCntrlDynamic property that you're binding to. This assumes that the ControlTemplate for your ContentControl is set up properly, including a ContentPresenter to receive and render the Content, which may not be the case with your custom DynamicContentControl.
I have a traditional form layout with a menu bar at the top and status bar at the bottom. When the user selects a menu item, the space in-between (the form's entire remaining client area) gets replaced with a user control - think of an SDI app that can host multiple types of documents.
If you know of a better way to go about this, please chime in. For now, I'm trying to get it to work in a very simplified version with a ContentControl, but I cannot get it to update the screen when its DataContext is set.
Here's the very simple code for ViewModelA. ViewModelB is identical, except for the Bs.
namespace Dynamic_ContentControl
{
public class ViewModelA: ViewModelBase
{
public ViewModelA()
{
DisplayName = "This is A";
}
}
}
The main window is very simple. It basically declares a property to hold the view model of the hosted control and exposes two commands to assign view models A or B.
namespace Dynamic_ContentControl
{
public class MainViewModel: ViewModelBase
{
private ViewModelBase clientContent = null;
public ICommand ShowA { get; private set; }
public ICommand ShowB { get; private set; }
public ViewModelBase ClientContent {
get
{
return clientContent;
}
private set
{
clientContent = value;
OnPropertyChanged("ClientContent");
}
}
public MainViewModel()
{
ShowA = new RelayCommand((obj) =>
{
ClientContent = new ViewModelA();
});
ShowB = new RelayCommand((obj) =>
{
ClientContent = new ViewModelB();
});
}
}
}
Finally, the XAML declares a ContentControl and sets its ContentTemplate to a DataTemplate called ClientAreaTemplate, whose ContentPresenter points to another DataTemplate, named TextBlockLayout:
<Window x:Class="Dynamic_ContentControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Dynamic_ContentControl"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Key="TextBlockLayout">
<TextBlock Text="{Binding Path=DisplayName}" />
</DataTemplate>
<DataTemplate x:Key="ButtonLayout">
<Button Content="{Binding Path=DisplayName}" />
</DataTemplate>
<DataTemplate x:Key="CheckBoxLayout">
<CheckBox Content="{Binding Path=DisplayName}" />
</DataTemplate>
<DataTemplate x:Key="ClientAreaTemplate">
<ContentPresenter x:Name="ContentArea" ContentTemplate="{StaticResource ResourceKey=TextBlockLayout}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=DataContext}"
Value="{x:Type vm:ViewModelB}">
<Setter TargetName="ContentArea"
Property="ContentTemplate"
Value="{StaticResource ResourceKey=ButtonLayout}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=DataContext}"
Value="{x:Type vm:ViewModelB}">
<Setter TargetName="ContentArea"
Property="ContentTemplate"
Value="{StaticResource ResourceKey=CheckBoxLayout}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<Grid>
<Button Content="Show A"
Command="{Binding Path=ShowA}"
HorizontalAlignment="Left"
Margin="10,10,0,0"
VerticalAlignment="Top"
Width="75" />
<Button Content="Show B"
Command="{Binding ShowB}"
HorizontalAlignment="Left"
Margin="90,10,0,0"
VerticalAlignment="Top"
Width="75" />
<Label Content="{Binding Path=ClientContent.DisplayName}"
HorizontalAlignment="Left"
Margin="170,8,0,0"
VerticalAlignment="Top" />
<ContentControl DataContext="{Binding Path=ClientContent}"
Content="{Binding}"
ContentTemplate="{StaticResource ResourceKey=ClientAreaTemplate}"
HorizontalAlignment="Left"
Margin="10,37,0,0"
VerticalAlignment="Top"
Height="198"
Width="211" />
</Grid>
</Window>
Expected behaviour
When the screen opens, I want TextBoxLayout to display. If the user then clicks on one of the two buttons, it should load either a ButtonLayout or CheckBoxLayout, depending on the actual runtime type of the view model that is assigned.
Actual behaviour
The screen opens with the TextBoxLayout loaded, but it never changes to another type as I click the buttons.
I think the problem is the way the DataTrigger tries to compare with the type, but there are no binding messages at all the Output window.
In this situation, you need to use DataTemplateSelector:
Provides a way to choose a DataTemplate based on the data object and the data-bound element.
Here is a version of dynamic DataTemplateSelector which returns a desired DataTemplate depending on the type:
/// <summary>
/// Provides a means to specify DataTemplates to be selected from within WPF code
/// </summary>
public class DynamicTemplateSelector : DataTemplateSelector
{
/// <summary>
/// Generic attached property specifying <see cref="Template"/>s
/// used by the <see cref="DynamicTemplateSelector"/>
/// </summary>
/// <remarks>
/// This attached property will allow you to set the templates you wish to be available whenever
/// a control's TemplateSelector is set to an instance of <see cref="DynamicTemplateSelector"/>
/// </remarks>
public static readonly DependencyProperty TemplatesProperty =
DependencyProperty.RegisterAttached("Templates", typeof(TemplateCollection), typeof(DataTemplateSelector),
new FrameworkPropertyMetadata(new TemplateCollection(), FrameworkPropertyMetadataOptions.Inherits));
/// <summary>
/// Gets the value of the <paramref name="element"/>'s attached <see cref="TemplatesProperty"/>
/// </summary>
/// <param name="element">The <see cref="UIElement"/> who's attached template's property you wish to retrieve</param>
/// <returns>The templates used by the givem <paramref name="element"/>
/// when using the <see cref="DynamicTemplateSelector"/></returns>
public static TemplateCollection GetTemplates(UIElement element)
{
return (TemplateCollection)element.GetValue(TemplatesProperty);
}
/// <summary>
/// Sets the value of the <paramref name="element"/>'s attached <see cref="TemplatesProperty"/>
/// </summary>
/// <param name="element">The element to set the property on</param>
/// <param name="collection">The collection of <see cref="Template"/>s to apply to this element</param>
public static void SetTemplates(UIElement element, TemplateCollection collection)
{
element.SetValue(TemplatesProperty, collection);
}
/// <summary>
/// Overriden base method to allow the selection of the correct DataTemplate
/// </summary>
/// <param name="item">The item for which the template should be retrieved</param>
/// <param name="container">The object containing the current item</param>
/// <returns>The <see cref="DataTemplate"/> to use when rendering the <paramref name="item"/></returns>
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
//This should ensure that the item we are getting is in fact capable of holding our property
//before we attempt to retrieve it.
if (!(container is UIElement))
return base.SelectTemplate(item, container);
//First, we gather all the templates associated with the current control through our dependency property
TemplateCollection templates = GetTemplates(container as UIElement);
if (templates == null || templates.Count == 0)
base.SelectTemplate(item, container);
//Then we go through them checking if any of them match our criteria
foreach (var template in templates)
//In this case, we are checking whether the type of the item
//is the same as the type supported by our DataTemplate
if (template.Value.IsInstanceOfType(item))
//And if it is, then we return that DataTemplate
return template.DataTemplate;
//If all else fails, then we go back to using the default DataTemplate
return base.SelectTemplate(item, container);
}
}
/// <summary>
/// Holds a collection of <see cref="Template"/> items
/// for application as a control's DataTemplate.
/// </summary>
public class TemplateCollection : List<Template>
{
}
/// <summary>
/// Provides a link between a value and a <see cref="DataTemplate"/>
/// for the <see cref="DynamicTemplateSelector"/>
/// </summary>
/// <remarks>
/// In this case, our value is a <see cref="System.Type"/> which we are attempting to match
/// to a <see cref="DataTemplate"/>
/// </remarks>
public class Template : DependencyObject
{
/// <summary>
/// Provides the value used to match this <see cref="DataTemplate"/> to an item
/// </summary>
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(Type), typeof(Template));
/// <summary>
/// Provides the <see cref="DataTemplate"/> used to render items matching the <see cref="Value"/>
/// </summary>
public static readonly DependencyProperty DataTemplateProperty =
DependencyProperty.Register("DataTemplate", typeof(DataTemplate), typeof(Template));
/// <summary>
/// Gets or Sets the value used to match this <see cref="DataTemplate"/> to an item
/// </summary>
public Type Value
{ get { return (Type)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } }
/// <summary>
/// Gets or Sets the <see cref="DataTemplate"/> used to render items matching the <see cref="Value"/>
/// </summary>
public DataTemplate DataTemplate
{ get { return (DataTemplate)GetValue(DataTemplateProperty); } set { SetValue(DataTemplateProperty, value); } }
}
Example of using
<local:DynamicTemplateSelector x:Key="MyTemplateSelector" />
<DataTemplate x:Key="StringTemplate">
<TextBlock>
<Run Text="String: " />
<Run Text="{Binding}" />
</TextBlock>
</DataTemplate>
<DataTemplate x:Key="Int32Template">
<TextBlock>
<Run Text="Int32: " />
<Run Text="{Binding}" />
</TextBlock>
</DataTemplate>
<Style x:Key="MyListStyle" TargetType="ListView">
<Setter Property="ItemTemplateSelector" Value="{StaticResource MyTemplateSelector}"/>
<Setter Property="local:DynamicTemplateSelector.Templates">
<Setter.Value>
<local:Templates>
<local:Template Value={x:Type String} DataTemplate={StaticResource StringTemplate}/>
<local:Template Value={x:Type Int32} DataTemplate={StaticResource Int32Template}/>
</local:Templates>
</Setter.Value>
</Setter>
</Style>