I develop some application. And I've found some strange things related to the DateTime format. So I've created some test application to check it in more detail.
So, my test application has the next structure:
Custom object class with only Date property:
public class MyObject
{
public DateTime Date { get; private set; }
public MyObject(DateTime date)
{
Date = date;
}
}
Custom ViewModel class:
public class MyViewModel : INotifyPropertyChanged
{
public virtual ICollectionView TableView
{
get => tableView;
set
{
tableView = value;
OnPropertyChanged(nameof(TableView));
}
}
public virtual ObservableCollection<MyObject> TableItems
{
get { return tableItems; }
set
{
tableItems = value;
OnPropertyChanged(nameof(TableItems));
TableView = CollectionViewSource.GetDefaultView(tableItems);
}
}
public MyViewModel()
{
var dateTimes = new List<MyObject>()
{
new MyObject(DateTime.MinValue),
new MyObject(DateTime.Now),
new MyObject(DateTime.MaxValue)
};
TableItems = new ObservableCollection<MyObject>(dateTimes);
}
private ICollectionView tableView;
private ObservableCollection<MyObject> tableItems;
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string prop = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
View control with DataGrid and ListView. Both of them have a binding to the same TableView collection:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="300"/>
<RowDefinition Height="300"/>
</Grid.RowDefinitions>
<DataGrid ItemsSource="{Binding TableView}">
</DataGrid>
<ListView Grid.Row="1" ItemsSource="{Binding TableView}">
<ListView.ItemTemplate>
<HierarchicalDataTemplate>
<CheckBox HorizontalContentAlignment="Left" VerticalContentAlignment="Center">
<CheckBox.Content>
<Label Content="{Binding Date}"/>
</CheckBox.Content>
</CheckBox>
</HierarchicalDataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
In this case I see a different view of the date in the table and in the list:
In case when I change the Label to the TextBlock in the ListView item template I will see the same result:
Why did it happen? And how can I display the same format in all controls according to the Culture date-time settings?
While ContentControl (and therefore Label too) uses the ContentControl.ContentStringFormat property to format string content, the text elements like TextBlock or TextBox use the FrameworkElement.Language property, which defaults to "en-US".
ContentControl (and Label)
In case of the ContentControl, where ContentControl.ContentStringFormat is not explicitly defined, the string is formatted using the Thread.CurrentThread.CurrentCulture as fallback CultureInfo (using the DateTimeFormatInfo returned from the CultureInfo.DateTimeFormat property).
In your case this is obviously not the language "en-US".
If you want to enforce a common date format on particular ContentControl elements, you can use a named Style to set ContentControl.ContentStringFormat property. To set the date format globally , you can set the Thread.CurrentThread.CurrentCulture property:
App.xaml.cs
class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var globalCultureInfo = CultureInfo.GetCultureInfo("en-GB");
/* Consider to set the CultureInfo.DefaultThread properties too */
Thread.CurrentThread.CurrentCulture = globalCultureInfo;
//Thread.CurrentThread.CurrentUICulture = globalCultureInfo;
}
}
If you don't want to change the thread culture, you can use an implicit Style that targets ContentControl and define it in App.xaml:
<Style TargetType="ContentControl">
<Setter Property="ContentStringFormat"
Value="dd.MM.yyyy" />
</Style>
<Style TargetType="Label"
BasedOn="{StaticResource {x:Type ContentControl}}" />
TextBlock and TextBox
For text elements you have to set or bind the FrameworkElement.Language property, for example using an implicit Style defined in App.xaml to apply the language globally:
<Style TargetType="ContentControl">
<Setter Property="Language"
Value="en-GB" />
</Style>
Alternatively, you can set the Binding.StringFormat property or use a IValueConverter. But both solutions are not helpful if you need to configure the date format globally.
FrameworkElement (ContentControl and TextBlock etc.)
To set the language for all FrameworkElement types, you must override the FrameworkElement.Language property in order to override the default value (which is "en-US"):
App.xaml.cs
class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var globalCultureInfo = CultureInfo.GetCultureInfo("en-GB");
/* Consider to set the Thread.CurrentThread
and CultureInfo.DefaultThread properties too */
XmlLanguage defaultFrameworkLanguage = XmlLanguage.GetLanguage(globalCultureInfo.Name);
FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(defaultFrameworkLanguage));
}
}
Related
I am creating a WPF-based plugin (for Revit, an architectural 3D modelling software, but this shouldn't matter) which is quite complex and I'm getting kind of lost.
The WPF Window is composed by 2 tabs and each Tab is a custom UserControl that I'm inserting in the TabItem through a Frame. The Main Window has a ViewModel where the data is bound.
One of the tabs helps with the creation of floors in a 3D model
part of MainWindow.xaml
<TabItem Name="LevelsTab" Header="Levels" HorizontalContentAlignment="Left">
<ScrollViewer >
<Frame Name="LevelsContent" Source="LevelsTab.xaml"/>
</ScrollViewer>
</TabItem>
The LevelsTab.xaml UserControl is really barebone and just contains buttons to create or remove a custom UserControl I created to represent graphically a floor in the UI (screenshot below). This very simple as well:
LevelDefinition.xaml
<UserControl x:Class="RevitPrototype.Setup.LevelDefinition" ....
<Label Grid.Column="0" Content="Level:"/>
<TextBox Name="LevelName" Text={Binding <!--yet to be bound-->}/>
<TextBox Name="LevelElevation" Text={Binding <!--yet to be bound-->}/>
<TextBox Name="ToFloorAbove" Text={Binding <!--yet to be bound-->}/>
</UserControl>
When the user clicks the buttons to add or remove floors in LevelsTab.xaml, a new LevelDefinition is added or removed to the gird.
Each LevelDefinition will be able to create a Level object from the information contained in the different TextBox elements, using MVVM. Eventually, in the ViewModel, I should have a List<Level> I guess.
Level.cs
class Level
{
public double Elevation { get; set; }
public string Name { get; set; }
public string Number { get; set; }
}
Each LevelDefinition should be sort of bound to the previous one though, as the floor below contains the information of the height to the Level above. The right-most TextBox in LevelDefinition.xaml indicated the distance between the current floor and the floor above, hence the Height `TextBox should just be the sum of its height PLUS the distance to the level above:
Of course the extra level of difficulty here is that if I change distance to the level above in one floor, all the floors above will have to update the height. For example: I change LEVEL 01 (from the pic) to have 4 meters to the level above, LEVEL 02's height will have to update to become 7m (instead of 6) and LEVEL 03's will have to become 10m.
But at this point I'm very lost:
How do I get this logic of getting the floor height bound to the info in the floor below?
How do I implement MVVM correctly in this case?
I hope I managed to explain the situation correctly even though it's quite complex and thanks for the help!
If you intend to make your Level items editable, you have to implement INotifyPropertyChanged. I created a level view model for demonstration purposes and added a property OverallElevation that represents the current elevation including that of previous levels.
public class LevelViewModel : INotifyPropertyChanged
{
private string _name;
private int _number;
private double _elevation;
private double _overallElevation;
public LevelViewModel(string name, int number, double elevation, double overallElevation)
{
Number = number;
Name = name;
Elevation = elevation;
OverallElevation = overallElevation;
}
public string Name
{
get => _name;
set
{
if (_name == value)
return;
_name = value;
OnPropertyChanged();
}
}
public int Number
{
get => _number;
set
{
if (_number == value)
return;
_number = value;
OnPropertyChanged();
}
}
public double Elevation
{
get => _elevation;
set
{
if (_elevation.CompareTo(value) == 0)
return;
_elevation = value;
OnPropertyChanged();
}
}
public double OverallElevation
{
get => _overallElevation;
set
{
if (_overallElevation.CompareTo(value) == 0)
return;
_overallElevation = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
You can bind these properties to your LevelDefinition user control. I adapted your sample, because it is incomplete. Since the overall elevation is calculated, I set the corresponding TextBox to be read-only, but you should really use a TextBlock or a similar read-only control instead.
<UserControl x:Class="RevitPrototype.Setup.LevelDefinition"
...>
<UserControl.Resources>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Margin" Value="5"/>
</Style>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="Level:"/>
<TextBox Grid.Column="1" Name="LevelName" Text="{Binding Name}"/>
<TextBox Grid.Column="2" Name="LevelElevation" Text="{Binding OverallElevation}" IsReadOnly="True"/>
<TextBox Grid.Column="3" Name="ToFloorAbove" Text="{Binding Elevation}"/>
</Grid>
</UserControl>
Since you did not provide your tab view model, I created one for reference. This view model exposes an ObservableCollection of levels, a GroundFloor property and commands to add and remove levels. I use a DelegateCommand type, but you may use a different one.
On each add of a level, you subscribe to the PropertyChanged event of the new level and on removal you unsubscribe to prevent memory leaks. Now, whenever a property changes on a LevelViewModel instance, the OnLevelPropertyChanged method is called. This method checks, if the Elevation property was changed. If it was, the UpdateOverallElevation method is called, which recalculates all overall elevation properties. Of course you could optimize this to only recalculate the levels above the current one passed as sender.
For a more robust implementation, you should subscribe to the CollectionChanged event of the Levels collection, so can subscribe to and unsubscribe from the PropertyChanged events of level items whenever you add, remove or modify the collection in other ways than through the commands like restoring a persisted collection.
public class LevelsViewModel
{
private const string GroundName = "GROUND FLOOR";
private const string LevelName = "LEVEL";
public ObservableCollection<LevelViewModel> Levels { get; }
public LevelViewModel GroundFloor { get; }
public ICommand Add { get; }
public ICommand Remove { get; }
public LevelsViewModel()
{
Levels = new ObservableCollection<LevelViewModel>();
GroundFloor = new LevelViewModel(GroundName, 0, 0, 0);
Add = new DelegateCommand<string>(ExecuteAdd);
Remove = new DelegateCommand(ExecuteRemove);
GroundFloor.PropertyChanged += OnLevelPropertyChanged;
}
private void ExecuteAdd(string arg)
{
if (!double.TryParse(arg, out var value))
return;
var lastLevel = Levels.Any() ? Levels.Last() : GroundFloor;
var number = lastLevel.Number + 1;
var name = GetDefaultLevelName(number);
var overallHeight = lastLevel.OverallElevation + value;
var level = new LevelViewModel(name, number, value, overallHeight);
level.PropertyChanged += OnLevelPropertyChanged;
Levels.Add(level);
}
private void ExecuteRemove()
{
if (!Levels.Any())
return;
var lastLevel = Levels.Last();
lastLevel.PropertyChanged -= OnLevelPropertyChanged;
Levels.Remove(lastLevel);
}
private void OnLevelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(LevelViewModel.Elevation))
return;
UpdateOverallElevation();
}
private static string GetDefaultLevelName(int number)
{
return $"{LevelName} {number:D2}";
}
private void UpdateOverallElevation()
{
GroundFloor.OverallElevation = GroundFloor.Elevation;
var previousLevel = GroundFloor;
foreach (var level in Levels)
{
level.OverallElevation = previousLevel.OverallElevation + level.Elevation;
previousLevel = level;
}
}
}
The view for the levels tab item could look like below. You can use a ListBox with your LevelDefinition user control as item template to display the levels. Alternatively, you could use a DataGrid with editable columns for each property of the LevelViewModel, which would be more flexible for users.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Levels}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<local:LevelDefinition/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListView>
<DockPanel Grid.Row="1" Margin="5">
<Button DockPanel.Dock="Right" Content="-" MinWidth="50" Command="{Binding Remove}"/>
<Button DockPanel.Dock="Right" Content="+" MinWidth="50" Command="{Binding Add}" CommandParameter="{Binding Text, ElementName=NewLevelElevationTextBox}"/>
<TextBox x:Name="NewLevelElevationTextBox" MinWidth="100"/>
</DockPanel>
<local:LevelDefinition Grid.Row="2" DataContext="{Binding GroundFloor}"/>
</Grid>
This is a simplified example, there is no input validation, invalid values are ignored on adding.
I've managed to implement this using a multi-binding converter.
Assuming that you set up the multi-converter as a static resource somewhere, the TextBlock to display the value is:
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ElevationMultiConverter}">
<MultiBinding.Bindings>
<Binding Path="" />
<Binding Path="DataContext.Levels" RelativeSource="{RelativeSource AncestorType={x:Type ItemsControl}}" />
</MultiBinding.Bindings>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
The converter itself looks like this:
class ElevationMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var item = values[0] as Level;
var list = values[1] as IList<Level>;
var lowerLevels = list.Where(listItem => list.IndexOf(listItem) <= list.IndexOf(item));
var elevation = lowerLevels.Sum(listItem => listItem.Height);
return elevation.ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
In this example, it depends on the specific order of items in the list to determine whether a level is above or below another; you could use a property, or whatever else.
I didn't use a framework for this example so I needed to implement INotifyPropertyChanged everywhere myself. In the MainViewModel, this meant adding a listener to each Level element's PropertyChanged event to trigger the multibinding converter to have 'changed'. In total, my MainViewModel looked like this:
class MainViewModel :INotifyPropertyChanged
{
public ObservableCollection<Level> Levels { get; set; }
public MainViewModel()
{
Levels = new ObservableCollection<Level>();
Levels.CollectionChanged += Levels_CollectionChanged;
}
private void Levels_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach(var i in e.NewItems)
{
(i as Level).PropertyChanged += MainViewModel_PropertyChanged;
}
}
private void MainViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Levels)));
}
public event PropertyChangedEventHandler PropertyChanged;
}
How it works:
A new Level is added to the collection, and it's PropertyChanged event is listened to by the containing view model. When the height of a level changes, the PropertyChanged event is fired and is picked up by the MainViewModel. It in turn fires a PropertyChanged event for the Levels property. The MultiConverter is bound to the Levels property, and all changes for it trigger the converters to re-evaluate and update all of the levels combined height values.
I have an application in which I set the content of a contentpresenter, dependent on the datatype by a datatemplate (see MainWindow). The Datatemplate is a usercontrol, which is actually datatype specific. (The small example below is only for demonstration, but in my "real" application the user shall be able to switch between different data.)
The usercontrol (UserControl1) has a DependencyProperty which I assign a value (in my application this is actually a binding to a VM, just set it to a string in example for simplicity).
Setting the value is still working fine. However In my UserControl I need to react to changes of the DependencyProperty to change the view of my UserControl (or later on CustomControl). So I implemented a OnPropertyChangend method.
When application starts OnPropertyChanged works as I expect it and I get the "correct" newvalue of my DependencyProperty. However, if I change my VM (i.e. my datatemplate changes) during runtime by clicking on a button, OnPropertyChanged returns the DependencyProperty's defaultvalue.
In my small example application, I can see that the value is set correctly, as the Textblock content changes to the correct value.
It only seems that OnPropertyChanged gets fired before my DependencyProperty's value gets the new value. So, it's not possible for me to react on the new value.
It is not really clear why this happens. Seems to have something to do with the order in which WPF resolves internal stuff?
Does anyone have a clue, how I can fix this behavior and get access to the current/last value when changing my VM and don't miss an update? As stated out before, I need to react on that value.
Maybe I am doing something totally stupid here. Is the approach I decided to use here a bad one? Are DataTemplates the wrong approach to switch between two pairs? What would be a better approach then? However, I guess it won't be possible to avoid the DependencyProperty and the UserControl in my application.
MainWindow.xaml
<!--MainWindow.xaml -->
<Grid>
<StackPanel>
<Button Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
<ContentPresenter Content="{Binding ActiveVM}">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type local:VM1}">
<local:UserControl1 MyProperty="Test1"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:VM2}">
<local:UserControl1 MyProperty="Test2"/>
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
</StackPanel>
</Grid>
MainWindow.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
vmParent = new VMParent();
DataContext = vmParent;
var vm1 = new VM1();
var vm2 = new VM2();
}
VMParent vmParent;
private void Button_Click(object sender, RoutedEventArgs e)
{
vmParent.ChangeActiveVM();
}
}
UserControl1.xaml
<!--UserControl1.xaml -->
<TextBlock Text="{Binding MyProperty, RelativeSource={RelativeSource AncestorType={x:Type local:UserControl1}}}"/>
UserControl1.cs
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public string MyProperty
{
get { return (string)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(string), typeof(UserControl1), new PropertyMetadata("DefaultString", OnMyPropertyChangend));
private static void OnMyPropertyChangend(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == "DefaultString")
{
;
//xxxxxx
//unexpectedly i get stuck here
//Would expect/need NewValue to be Text1/Text2 to react to it
//xxxxxx
}
}
}
VMParent
class VMParent : INotifyPropertyChanged
{
public VMParent()
{
vm1 = new VM1();
vm2 = new VM2();
ActiveVM = vm1;
}
public event PropertyChangedEventHandler PropertyChanged;
VM1 vm1;
VM2 vm2;
public object ActiveVM
{
get => m_activeVM;
set { m_activeVM = value; OnPropertyChanged("ActiveVM"); }
}
private object m_activeVM;
protected internal void OnPropertyChanged(string propertyname)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
}
public void ChangeActiveVM()
{
if (ActiveVM is VM1)
ActiveVM = vm2;
else
ActiveVM = vm1;
}
}
VMs are only used to apply Datatemplate
class VM1
{
}
class VM2
{
}
I need both operating by mouse clicking and operating by hotkeys in my WPF application. User's actions affects on both data and appearance of application controls.
For example, the following app will send data to tea machine. You can select the tea brand, type (hot or cold) and optional ingredients: milk, lemon and syrup.
Not good from the point of view of UI design, but just example:
If to click the dropdown menu or input Ctrl+B, the list of select options will appear.
If to click the "Hot" button on input Ctrl+T, button becomes blue and text becomes "Cold". If to click or input Ctrl+T again, button becomes orange and text becomes to "Hot" again.
If to click optional ingredient button or input respective shortcut, button's background and text becomes gray (it means "unselected"). Same action will return the respective button to active state.
If don't use MVVM and don't define shortcuts, the logic will be relatively simple:
Tea tea = new Tea(); // Assume that default settings avalible
private void ToggleTeaType(object sender, EventArgs e){
// Change Data
if(tea.getType().Equals("Hot")){
tea.setType("Cold");
}
else{
tea.setType("Hot");
}
// Change Button Appearence
ChangeTeaTypeButtonAppearence(sender, e);
}
private void ChangeTeaTypeButtonAppearence(object sender, EventArgs e){
Button clickedButton = sender as Button;
Style hotTeaButtonStyle = this.FindResource("TeaTypeButtonHot") as Style;
Style coldTeaButtonStyle = this.FindResource("TeaTypeButtonCold") as Style;
if (clickedButton.Tag.Equals("Hot")) {
clickedButton.Style = coldTeaButtonStyle; // includes Tag declaration
clickedButton.Content = "Cold";
}
else (clickedButton.Tag.Equals("Cold")) {
clickedButton.Style = hotTeaButtonStyle; // includes Tag declaration
clickedButton.Content = "Hot";
}
}
// similarly for ingredients toggles
XAML:
<Button Content="Hot"
Tag="Hot"
Click="ToggleTeaType"
Style="{StaticResource TeaTypeButtonHot}"/>
<Button Content="Milk"
Tag="True"
Click="ToggleMilk"
Style="{StaticResource IngredientButtonTrue}"/>
<Button Content="Lemon"
Tag="True"
Click="ToggleLemon"
Style="{StaticResource IngredientButtonTrue}"/>
<Button Content="Syrup"
Tag="True"
Click="ToggleSyrup"
Style="{StaticResource IngredientButtonTrue}"/>
I changed my similar WPF project to MVVM because thanks to commands it's simple to assign the shortcuts:
<Window.InputBindings>
<KeyBinding Gesture="Ctrl+T" Command="{Binding ToggleTeaType}" />
</Window.InputBindings>
However, now it's a problem how to set the control's appearance. The following code is invalid:
private RelayCommand toggleTeaType;
public RelayCommand ToggleTeaType {
// change data by MVVM methods...
// change appearence:
ChangeTeaTypeButtonAppearence(object sender, EventArgs e);
}
I need the Relay Commands because I can bind it to both buttons and shortcuts, but how I can access to View controls from RelayCommand?
You should keep the viewmodel clean of view specific behavior. The viewmodel should just provide an interface for all relevant settings, it could look similar to the following (BaseViewModel would contain some helper methods to implement INotifyPropertyChanged etc.):
public class TeaConfigurationViewModel : BaseViewModel
{
public TeaConfigurationViewModel()
{
_TeaNames = new string[]
{
"Lipton",
"Generic",
"Misc",
};
}
private IEnumerable<string> _TeaNames;
public IEnumerable<string> TeaNames
{
get { return _TeaNames; }
}
private string _SelectedTea;
public string SelectedTea
{
get { return _SelectedTea; }
set { SetProperty(ref _SelectedTea, value); }
}
private bool _IsHotTea;
public bool IsHotTea
{
get { return _IsHotTea; }
set { SetProperty(ref _IsHotTea, value); }
}
private bool _WithMilk;
public bool WithMilk
{
get { return _WithMilk; }
set { SetProperty(ref _WithMilk, value); }
}
private bool _WithLemon;
public bool WithLemon
{
get { return _WithLemon; }
set { SetProperty(ref _WithLemon, value); }
}
private bool _WithSyrup;
public bool WithSyrup
{
get { return _WithSyrup; }
set { SetProperty(ref _WithSyrup, value); }
}
}
As you see, there is a property for each setting, but the viewmodel doesn't care about how the property is assigned.
So lets build some UI. For the following example, generally suppose xmlns:local points to your project namespace.
I suggest utilizing a customized ToggleButton for your purpose:
public class MyToggleButton : ToggleButton
{
static MyToggleButton()
{
MyToggleButton.DefaultStyleKeyProperty.OverrideMetadata(typeof(MyToggleButton), new FrameworkPropertyMetadata(typeof(MyToggleButton)));
}
public Brush ToggledBackground
{
get { return (Brush)GetValue(ToggledBackgroundProperty); }
set { SetValue(ToggledBackgroundProperty, value); }
}
// Using a DependencyProperty as the backing store for ToggledBackground. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ToggledBackgroundProperty =
DependencyProperty.Register("ToggledBackground", typeof(Brush), typeof(MyToggleButton), new FrameworkPropertyMetadata());
}
And in Themes/Generic.xaml:
<Style TargetType="{x:Type local:MyToggleButton}" BasedOn="{StaticResource {x:Type ToggleButton}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyToggleButton}">
<Border x:Name="border1" BorderBrush="Gray" BorderThickness="1" Background="{TemplateBinding Background}" Padding="5">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="border1" Property="Background" Value="{Binding ToggledBackground,RelativeSource={RelativeSource TemplatedParent}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Now, build the actual window content using this toggle button. This is just a rough sketch of your desired UI, containing only the functional controls without labels and explanation:
<Grid x:Name="grid1">
<StackPanel>
<StackPanel Orientation="Horizontal">
<ComboBox
x:Name="cb1"
VerticalAlignment="Center"
IsEditable="True"
Margin="20"
MinWidth="200"
ItemsSource="{Binding TeaNames}"
SelectedItem="{Binding SelectedTea}">
</ComboBox>
<local:MyToggleButton
x:Name="hotToggle"
IsChecked="{Binding IsHotTea}"
VerticalAlignment="Center"
Margin="20" MinWidth="60"
Background="AliceBlue" ToggledBackground="Orange">
<local:MyToggleButton.Style>
<Style TargetType="{x:Type local:MyToggleButton}">
<Setter Property="Content" Value="Cold"/>
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Content" Value="Hot"/>
</Trigger>
</Style.Triggers>
</Style>
</local:MyToggleButton.Style>
</local:MyToggleButton>
</StackPanel>
<StackPanel Orientation="Horizontal">
<local:MyToggleButton
x:Name="milkToggle"
Content="Milk"
IsChecked="{Binding WithMilk}"
VerticalAlignment="Center"
Margin="20" MinWidth="60"
Background="WhiteSmoke" ToggledBackground="LightGreen"/>
<local:MyToggleButton
x:Name="lemonToggle"
Content="Lemon"
IsChecked="{Binding WithLemon}"
VerticalAlignment="Center"
Margin="20" MinWidth="60"
Background="WhiteSmoke" ToggledBackground="LightGreen"/>
<local:MyToggleButton
x:Name="syrupToggle"
Content="Syrup"
IsChecked="{Binding WithSyrup}"
VerticalAlignment="Center"
Margin="20" MinWidth="60"
Background="WhiteSmoke" ToggledBackground="LightGreen"/>
</StackPanel>
</StackPanel>
</Grid>
Notice the style trigger to change the button content between Hot and Cold.
Initialize the datacontext somewhere (eg. in the window constructor)
public MainWindow()
{
InitializeComponent();
grid1.DataContext = new TeaConfigurationViewModel();
}
At this point, you have a fully functional UI, it will work with the default mouse and keyboard input methods, but it won't yet support your shortcut keys.
So lets add the keyboard shortcuts without destroying the already-working UI. One approach is, to create and use some custom commands:
public static class AutomationCommands
{
public static RoutedCommand OpenList = new RoutedCommand("OpenList", typeof(AutomationCommands), new InputGestureCollection()
{
new KeyGesture(Key.B, ModifierKeys.Control)
});
public static RoutedCommand ToggleHot = new RoutedCommand("ToggleHot", typeof(AutomationCommands), new InputGestureCollection()
{
new KeyGesture(Key.T, ModifierKeys.Control)
});
public static RoutedCommand ToggleMilk = new RoutedCommand("ToggleMilk", typeof(AutomationCommands), new InputGestureCollection()
{
new KeyGesture(Key.M, ModifierKeys.Control)
});
public static RoutedCommand ToggleLemon = new RoutedCommand("ToggleLemon", typeof(AutomationCommands), new InputGestureCollection()
{
new KeyGesture(Key.L, ModifierKeys.Control)
});
public static RoutedCommand ToggleSyrup = new RoutedCommand("ToggleSyrup", typeof(AutomationCommands), new InputGestureCollection()
{
new KeyGesture(Key.S, ModifierKeys.Control)
});
}
You can then bind those commands to appropriate actions in your main window:
<Window.CommandBindings>
<CommandBinding Command="local:AutomationCommands.OpenList" Executed="OpenList_Executed"/>
<CommandBinding Command="local:AutomationCommands.ToggleHot" Executed="ToggleHot_Executed"/>
<CommandBinding Command="local:AutomationCommands.ToggleMilk" Executed="ToggleMilk_Executed"/>
<CommandBinding Command="local:AutomationCommands.ToggleLemon" Executed="ToggleLemon_Executed"/>
<CommandBinding Command="local:AutomationCommands.ToggleSyrup" Executed="ToggleSyrup_Executed"/>
</Window.CommandBindings>
and implement the appropriate handler method for each shortcut in the window code behind:
private void OpenList_Executed(object sender, ExecutedRoutedEventArgs e)
{
FocusManager.SetFocusedElement(cb1, cb1);
cb1.IsDropDownOpen = true;
}
private void ToggleHot_Executed(object sender, ExecutedRoutedEventArgs e)
{
hotToggle.IsChecked = !hotToggle.IsChecked;
}
private void ToggleMilk_Executed(object sender, ExecutedRoutedEventArgs e)
{
milkToggle.IsChecked = !milkToggle.IsChecked;
}
private void ToggleLemon_Executed(object sender, ExecutedRoutedEventArgs e)
{
lemonToggle.IsChecked = !lemonToggle.IsChecked;
}
private void ToggleSyrup_Executed(object sender, ExecutedRoutedEventArgs e)
{
syrupToggle.IsChecked = !syrupToggle.IsChecked;
}
Again, remember this whole input binding thing is purely UI related, it is just an alternative way to change the displayed properties and the changes will be transferred to the viewmodel with the same binding as if the user clicks the button by mouse. There is no reason to carry such things into the viewmodel.
how I can access to View controls from RelayCommand?
You shouldn't. The whole point of MVVM (arguably) is to separate concerns. The 'state' that the ViewModel contains is rendered by the View (controls). The ViewModel/logic should never directly adjust the view - as this breaks the separation of concerns and closely couples the logic to the rendering.
What you need is for the view to render how it wants to display the state in the View Model.
Typically, this is done by bindings. As example: Rather than the ViewModel grabbing a text box reference and setting the string: myTextBox.SetText("some value"), we have the view bind to the property MyText in the view model.
It's the view's responsibility to decide how to show things on the screen.
That's all well and good, but how? I suggest, if you want to do this change using styles like you describe, I'd try using a converter that converts the using a binding to ViewModel state (Say, an enum property Hot or Cold):
<Button Content="Hot"
Tag="Hot"
Click="ToggleTeaType"
Style="{Binding TeaType, Converter={StaticResource TeaTypeButtonStyleConverter}}"/>
Note, we're using WPF's bindings. The only reference we've got tot he view model is through it's property TeaType.
Defined in your static resources, we have the converter:
<ResourceDictionary>
<Style x:Key="HotTeaStyle"/>
<Style x:Key="ColdTeaStyle"/>
<local:TeaTypeButtonStyleConverter
x:Key="TeaTypeButtonStyleConverter"
HotStateStyle="{StaticResource HotTeaStyle}"
ColdStateStyle="{StaticResource ColdTeaStyle}"/>
</ResourceDictionary>
And have the logic for converting from the TeaType enum to a Style in this:
public enum TeaType
{
Hot, Cold
}
class TeaTypeButtonStyleConverter : IValueConverter
{
public Style HotStateStyle { get; set; }
public Style ColdStateStyle { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
TeaType teaType = (TeaType)value;
if (teaType == TeaType.Hot)
{
return HotStateStyle;
}
else if (teaType == TeaType.Cold)
{
return ColdStateStyle;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
It could be made more generic and re-usable.
You should also take a look at toggle buttons, they deal with this kind of thing internally.
I'm working on developing a custom user control for my application. This control is very simple. It's just a grid, with a checkbox in [0,0] and a TextBlock in [0,1]. I've had no issues getting it designed how I'd like in XAML.
However, the second step is giving me some trouble. I'm trying to expose the IsChecked bool? of my sub-control that is a Checkbox for binding on my mainform, and the same idea with the Text property of TextBlock.
I've tried a few different ways of going about this, but to no avail.
Here's the general code I have:
public partial class CDCheckBox : UserControl
{
public bool? IsChecked
{
get { return chk.IsChecked; }
set { chk.IsChecked = value; }
}
public string Text
{
get { return lbl.Text; }
set { lbl.Text = value; }
}
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.Register(
"IsChecked",
typeof(bool?),
typeof(CDCheckBox),
new PropertyMetadata(default(bool?), OnItemsPropertyChanged));
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(CDCheckBox),
new PropertyMetadata(default(string), OnItemsPropertyChanged));
/*
public event PropertyChangedEventHandler PropertyChanged;
private void OnNotify(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
*/
private static void OnItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// AutocompleteTextBox source = d as AutocompleteTextBox;
// Do something...
//lbl.Text = e.NewValue.ToString();
}
/*
public event PropertyChangedEventHandler PropertyChanged;
private void OnNotify(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
*/
public CDCheckBox()
{
InitializeComponent();
}
}
When I run the code above, I get no errors, but my binded data doesn't show up in my TextBlock control. When I tried before I wrote the depenency properties, it gave me an error in my XAML saying "A 'Binding' cannot be set on the 'IsChecked' property of type 'CDCheckBox'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject."
Interestingly however, this error does not appear in the constructor, but instead in the window_loaded method I've written. This appears to be a red herring however, as if I comment out that code, it still fails before the form can display with XAMLParse Error.
Further to my comment, you could try styling an existing control that has the property types that you need. For example, in your custom control you have a nullable Boolean property and a string property. If you repurpose a CheckBox control, it already has a nullable Boolean property (IsChecked) and an object property (Content) which can be used to hold a string.
Here's how you might restyle a CheckBox control and change its template to achieve the result you're after:
<Window x:Class="..."
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<Style x:Key="MySuperCheckboxStyle"
TargetType="{x:Type CheckBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0"
IsChecked="{TemplateBinding IsChecked}"
Content="Whatever you need here" />
<TextBlock Grid.Column="1"
Text="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel>
<CheckBox IsChecked="True"
Content="Unstyled check box"
Margin="10" />
<CheckBox Style="{StaticResource MySuperCheckboxStyle}"
IsChecked="True"
Content="Styled check box"
Margin="10" />
</StackPanel>
</Window>
Key here are the TemplateBinding bindings used in the control template. These bind not to a data context like in normal data binding, but rather to properties of the control being templated.
Whenever you find yourself wanting to create a custom control in WPF it is worth exploring whether you can take an existing control and change its appearance to suit what you need, as this is often less work than creating a new control (on the flipside it's not always possible to repurpose an existing control, particularly if you need different behaviour).
So here is the XAML that I have:
<ItemsControl ItemsSource="{Binding Path=Groups}" ItemTemplateSelector="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ListTemplateSelector}"/>
Here is my ListTemplateSelector class:
public class ListTemplateSelector : DataTemplateSelector {
public DataTemplate GroupTemplate { get; set; }
public DataTemplate ItemTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
GroupList<Person> list = item as GroupList<Person>;
if (list != null && !list.IsLeaf)
return GroupTemplate;
return ItemTemplate;
}
}
The GroupTemplate data template references the ListTemplateSelector inside itself, so this is why I have set up like I have it set up. It's the only recursive hack I could put together. But that's not the problem I'm having.
My problem is, I want to change from ItemTemplate to GroupTemplate when the IsLeaf property changes. This works beautifully the very first time since it reads the property the first time. But once this property changes, the template selector doesn't get reapplied. Now, I could use triggers to bind to the value and set the item template appropriately, but I need to be able to set a different template for each item, as they could be in a different state.
For instance, say I have a list of groups like this:
Group 1: IsLeaf = false, so template = GroupTemplate
Group 2: IsLeaf = true, so template = ItemTemplate
Group 3: IsLeaf = false, so template = GroupTemplate
And once group 1's IsLeaf property changes to true, the template needs to automatically change to ItemTemplate.
EDIT:
Here is my temporary solution. Any better way to do it?
<ItemsControl ItemsSource="{Binding Path=Groups}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="ContentTemplate" Value="{DynamicResource ItemTemplate}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsLeaf}" Value="False">
<Setter Property="ContentTemplate" Value="{DynamicResource GroupTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I found this workaround that seems easier to me. From within the TemplateSelector listen to the property that your care about and then reapply the template selector to force a refresh.
public class DataSourceTemplateSelector : DataTemplateSelector
{
public DataTemplate IA { get; set; }
public DataTemplate Dispatcher { get; set; }
public DataTemplate Sql { get; set; }
public override DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
var ds = item as DataLocationViewModel;
if (ds == null)
{
return base.SelectTemplate(item, container);
}
PropertyChangedEventHandler lambda = null;
lambda = (o, args) =>
{
if (args.PropertyName == "SelectedDataSourceType")
{
ds.PropertyChanged -= lambda;
var cp = (ContentPresenter)container;
cp.ContentTemplateSelector = null;
cp.ContentTemplateSelector = this;
}
};
ds.PropertyChanged += lambda;
switch (ds.SelectedDataSourceType.Value)
{
case DataSourceType.Dispatcher:
return Dispatcher;
case DataSourceType.IA:
return IA;
case DataSourceType.Sql:
return Sql;
default:
throw new NotImplementedException(ds.SelectedDataSourceType.Value.ToString());
}
}
}
Regarding your EDIT, wouldn't a DataTemplate Trigger be enough instead of using a Style? That is:
<ItemsControl ItemsSource="{Binding Path=Groups}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl x:Name="cc" Content="{Binding}" ContentTemplate="{DynamicResource ItemTemplate}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsLeaf}" Value="False">
<Setter TargetName="cc" Property="ContentTemplate" Value="{DynamicResource GroupTemplate}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Returning back to your original solution and the problem of "the template selector doesn't get reapplied": you can refresh your view like that
CollectionViewSource.GetDefaultView(YourItemsControl.ItemsSource).Refresh();
where for brevity sake your ItemsControl is referenced by its name ("YourItemsControl") added to your XAML:
<ItemsControl x:Name="YourItemsControl" ItemsSource="{Binding Path=Groups}"
ItemTemplateSelector="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ListTemplateSelector}"/>
The only problem may be how to choose right place in your project for this refresh instruction. It could go into a view code-behind, or, if your IsLeaf is a DP, the right place would be a dependency-property-changed callback.
I do it with a binding proxy.
It works like a normal binding proxy (but with 2 Props - copies data from DataIn to DataOut), but sets the DataOut to NULL and back to the DataIn value whenever the Trigger value changes:
public class BindingProxyForTemplateSelector : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxyForTemplateSelector();
}
#endregion
public object DataIn
{
get { return (object)GetValue(DataInProperty); }
set { SetValue(DataInProperty, value); }
}
public object DataOut
{
get { return (object) GetValue(DataOutProperty); }
set { SetValue(DataOutProperty, value); }
}
public object Trigger
{
get { return (object) GetValue(TriggerProperty); }
set { SetValue(TriggerProperty, value); }
}
public static readonly DependencyProperty TriggerProperty = DependencyProperty.Register(nameof(Trigger), typeof(object), typeof(BindingProxyForTemplateSelector), new PropertyMetadata(default(object), OnTriggerValueChanged));
public static readonly DependencyProperty DataInProperty = DependencyProperty.Register(nameof(DataIn), typeof(object), typeof(BindingProxyForTemplateSelector), new UIPropertyMetadata(null, OnDataChanged));
public static readonly DependencyProperty DataOutProperty = DependencyProperty.Register(nameof(DataOut), typeof(object), typeof(BindingProxyForTemplateSelector), new PropertyMetadata(default(object)));
private static void OnTriggerValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// this does the whole trick
var sender = d as BindingProxyForTemplateSelector;
if (sender == null)
return;
sender.DataOut = null; // set to null and then back triggers the TemplateSelector to search for a new template
sender.DataOut = sender.DataIn;
}
private static void OnDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var sender = d as BindingProxyForTemplateSelector;
if (sender == null)
return;
sender.DataOut = e.NewValue;
}
}
Use it like this:
<Grid>
<Grid.Resources>
<local:BindingProxyForTemplateSelector DataIn="{Binding}" Trigger="{Binding Item.SomeBool}" x:Key="BindingProxy"/>
</Grid.Resources>
<ContentControl Content="{Binding Source={StaticResource BindingProxy}, Path=DataOut.Item}" ContentTemplateSelector="{StaticResource TemplateSelector}"/>
</Grid>
So you don't bind to your DataContext directly, but to the BindingProxy's DataOut, which mirrors the original DataContext, but with a small difference: When the trigger changes (in this example a bool value inside the 'Item'), the TemplateSelector gets retriggered.
You don't have to change your TemplateSelector for this.
It is also possible to add more Triggers, just add a Trigger2.
I wasn't really satisfied with the solutions I will post the way I've managed to get the selector check for changes:
public class DynamicSelectorContentControl : ContentControl
{
// Using a DependencyProperty as the backing store for ListenToProperties. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ListenToPropertiesProperty =
DependencyProperty.Register("ListenToProperties", typeof(string),
typeof(DynamicSelectorContentControl),
new FrameworkPropertyMetadata(string.Empty));
public DynamicSelectorContentControl()
{
this.DataContextChanged += DynamicSelectorContentControl_DataContextChanged;
}
public string ListenToProperties
{
get { return (string)GetValue(ListenToPropertiesProperty); }
set { SetValue(ListenToPropertiesProperty, value); }
}
private void CheckForProperty(object sender, PropertyChangedEventArgs e)
{
if (ListenToProperties.Contains(e.PropertyName))
{
ClearSelector();
}
}
private void ClearSelector()
{
var oldSelector = this.ContentTemplateSelector;
if (oldSelector != null)
{
this.ContentTemplateSelector = null;
this.ContentTemplateSelector = oldSelector;
}
}
private void DynamicSelectorContentControl_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
var listOfProperties = ListenToProperties.Split(',').Select(s => s.Trim());
var oldObservable = e.OldValue as INotifyPropertyChanged;
if (oldObservable != null && listOfProperties.Any())
{
PropertyChangedEventManager.RemoveHandler(oldObservable, CheckForProperty, string.Empty);
}
var newObservable = e.NewValue as INotifyPropertyChanged;
if (newObservable != null && listOfProperties.Any())
{
PropertyChangedEventManager.AddHandler(newObservable, CheckForProperty, string.Empty);
}
if (e.OldValue != null)
{
ClearSelector();
}
}
}
Usage in XAML:
<controls:DynamicSelectorContentControl DockPanel.Dock="Top"
ContentTemplateSelector="{StaticResource AgeGenderSelector}"
ListenToProperties="Gender, Age"
Content="{Binding .}"/>
This could be changed to have the dependency be a list, but a string was better for my case.
It works well and has no memory leak. Besides, you can have you DataTemplates in an extra file which does not garbage your main xaml.
Cheers,
Marco