I have a view that I would like to assign a "backup" viewmodel to. Essentially if "Generic" is null I would like to set the DataContext to "GenericFactory". "GenericFactory" is able to create an instance of the "Generic" viewmodel. Upon creation the viewmodel is assigned to the appropriate property and the PropertyChanged event is fired, however given the code below the only DataContext I'm ever bound to is "GenericFactory". Can anyone explain and/or offer an alternative solution?
XAML
<Page x:Class="GenericProject.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vw="clr-namespace:GenericProject.View">
<StackPanel>
<!--Additional markup-->
<vw:GenericView>
<vw:GenericView.Style>
<Style TargetType="{x:Type vw:GenericView}">
<Setter Property="DataContext" Value="{Binding Generic}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Generic}" Value="{x:Null}">
<Setter Property="DataContext" Value="{Binding GenericFactory}" />
</DataTrigger>
</Style.Triggers>
</Style>
</vw:GenericView.Style>
</vw:GenericView>
</StackPanel>
</Page>
ViewModel
public class MainPageViewModel : ViewModelBase
{
public GenericViewModel Generic
{
get { return _generic; }
private set
{
if (_generic != value)
{
_generic = value;
base.OnPropertyChanged("Generic");
}
}
}
public GenericFactoryViewModel GenericFactory { get; private set; }
private void OnGenericFactoryCreatedGeneric(object sender, CreatedGenericEventArgs e)
{
Generic = e.Generic;
}
public MainPageViewModel()
{
GenericFactory = new GenericFactoryViewModel();
GenericFactory.CreatedGeneric += OnGenericFactoryCreatedGeneric;
}
}
Thanks - Derrick
Thanks to XAMIMAX's comment I was able to find a solution using PriorityBinding.
XAML
<Page x:Class="GenericProject.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GenericProject"
xmlns:vw="clr-namespace:GenericProject.View">
<Page.Resources>
<local:NullToDependencyPropertyUnsetConverter x:Key="NullToDependencyPropertyUnsetConverter" />
</Page.Resources>
<StackPanel>
<!--Additional markup-->
<vw:GenericView>
<vw:GenericView.DataContext>
<PriorityBinding>
<Binding Path="Generic" Converter="{StaticResource NullToDependencyPropertyUnsetConverter}" />
<Binding Path="GenericFactory" />
</PriorityBinding>
</vw:GenericView.DataContext>
</vw:GenericView>
</StackPanel>
</Page>
Converter
public class NullToDependencyPropertyUnsetConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value ?? DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I don't know how your factory works so this is probably not working code but you should be handling this logic in the view model, your view should just set the datacontext.
public GenericViewModel Generic
{
get
{
if(_generic == null)
{
GenericFactory.Create();
}
return _generic;
}
private set
{
if (_generic != value)
{
_generic = value;
base.OnPropertyChanged("Generic");
}
}
}
This will return null for Generic but when OnGenericFactoryCreatedGeneric is called it will set Generic and then cause the binding to update to a newly created view model.
If your factory has a synchronous create that returns the ViewModel then that would be better as Generic will never return null. _generic = GenericFactory.Create();
Related
I want to compare two dynamic values User_id and user_id for equality and setting one property Cursor. Also, when the cursor is hand, I have to execute one function. How to do it? This is the code that I am using:
<DataTrigger Binding="{Binding Path=User_id}" Value="{Binding Path=user_id}">
<Setter Property="Cursor" Value="Hand"/>
</DataTrigger>
There are a couple options to attack this.
#1. Multibinding Converter
You can use Multibindingto input the two values into a IMultiValueConverter. To use this type of binding in your DataTrigger, you would use follow the following syntax.
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding>
<MultiBinding.Converter>
<local:EqualityConverter />
</MultiBinding.Converter>
<Binding Path="User_id" />
<Binding Path="user_id" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Window.Cursor" Value="Hand"/>
</DataTrigger>
The MultiBinding.Converteris set to a new instance of EqualityConverter, which is a class I created that implements the IMultiValueConverter interface. This class will do the comparison for you. The DataTrigger triggers when this converter returns true.
public class EqualityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length < 2)
return false;
return values[0].Equals(values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
#2. MVVM Pattern
I'm not sure where your DataContext is coming from, but if possible, you may want to consider using a view model for your binding. The view model could expose a property that does the equality comparison for you. Something like this.
public class UserViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _User_id;
private int _user_id;
public int User_id
{
get
{
return _User_id;
}
set
{
if (_User_id != value)
{
_User_id = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("User_id"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsUserIdsEqual"));
DoSomething();
}
}
}
public int user_id
{
get
{
return _user_id;
}
set
{
if (_user_id != value)
{
_user_id = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("user_id"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsUserIdsEqual"));
DoSomething();
}
}
}
public bool IsUserIdsEqual
{
get { return _user_id == _User_id; }
}
private void DoSomething()
{
if (this.IsUserIdsEqual)
{
//Do something when they are equal.
}
}
}
If using a view model like this, your DataTrigger could simplify to..
<DataTrigger Binding="{Binding Path=IsUserIdsEqual}" Value="True">
<Setter Property="Window.Cursor" Value="Hand"/>
</DataTrigger>
Regarding executing a function on the trigger, I added a DoSomething method to highlight how the view model could be used to execute a function when the two IDs are equal. I'm not sure if that would work for your case because I'm not sure what the intent of the function call is, but it is a way to execute a function when a condition changes.
I have a user control with a TextBox that needs to be validated. The validation will vary according to the value of a dependency property in the UC, so I need to pass that as a parameter. To pass a parameter I'm using Passing a data-bound value to a validation rule as a guide. However, the binding I'm using doesn't work and I don't know why. I've beat my head against it, googled everything I can think of, no joy.
Here's the code. Hopefully I've provided enough ...
In the user control I have this XAML.
<TextBox Name="ValueBox"
PreviewMouseLeftButtonUp="OnPreviewMouseLeftButtonUp"
Height="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=Height}"
BorderThickness="0"
TextAlignment="Center"
VerticalContentAlignment="Center">
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=IsControlEnabled}"
Value="False">
<Setter Property="Background" Value="{StaticResource DisabledColor}"/>
</DataTrigger>
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=InteractionMode}"
Value="{x:Static local:TreatmentScheduleNumberBoxUserControl+InteractionModes.Select}">
<Setter Property="IsReadOnly" Value="True" />
<Setter Property="Cursor" Value="{x:Static Cursors.Hand}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
<TextBox.Resources>
<local:NumberBoxValueConverter x:Key="NumberBoxConverter"/>
</TextBox.Resources>
<TextBox.Text>
<tools:ConverterBindableParameter
Converter="{StaticResource NumberBoxConverter}"
ConverterParameterBinding="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=TreatmentLampType}">
<!--https://social.technet.microsoft.com/wiki/contents/articles/31422.wpf-passing-a-data-bound-value-to-a-validation-rule.aspx-->
<tools:ConverterBindableParameter.Binding>
<Binding RelativeSource="{RelativeSource AncestorType=UserControl}" Path="Value" FallbackValue="3">
<Binding.ValidationRules>
<local:NumberBoxValidationRule>
<local:NumberBoxValidationRule.Wrapper>
<local:Wrapper NumberBoxUsage1="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=NumberBoxUsage
, Converter={StaticResource DebugDummyConverter, PresentationTraceSources.TraceLevel=High}}" />
</local:NumberBoxValidationRule.Wrapper>
</local:NumberBoxValidationRule>
</Binding.ValidationRules>
</Binding>
</tools:ConverterBindableParameter.Binding>
</tools:ConverterBindableParameter>
</TextBox.Text>
</TextBox>
The problem lies in this binding, where NumberBoxUsage1 is a dependency property in validation environment and NumberBoxUsage is a dependency property in the UC.
<local:Wrapper NumberBoxUsage1="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=NumberBoxUsage
, Converter={StaticResource DebugDummyConverter, PresentationTraceSources.TraceLevel=High}}" />
When it runs, NumberBoxUsage1 remains the default and isn't assigned the value of NumberBoxUsage. I can change the binding to a literal assignment and that works. I've added a dummy converter, as shown, as well as PresentationTraceSourcesbut the converter is never called and there is no trace in the Output window. Any help appreciated.
I might add that everything else in this TextBox works fine. Here's the relevant C# stuff.
Wrapper
public class Wrapper : DependencyObject
{
public NumberBoxUsages NumberBoxUsage1 {
get => (NumberBoxUsages)GetValue(NumberBoxUsage1Property);
set => SetValue(NumberBoxUsage1Property, value);
}
public static readonly DependencyProperty NumberBoxUsage1Property =
DependencyProperty.Register(nameof(NumberBoxUsage1), typeof(NumberBoxUsages), typeof(Wrapper),
new FrameworkPropertyMetadata(
NumberBoxUsages.UvPrim,
(sender, e) =>
{
var dObj = sender as Wrapper;
var x = dObj.NumberBoxUsage1;
// leave for debugging help
}
));
}
NumberBoxValidationRule
public class NumberBoxValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value == null)
{
return new ValidationResult(false, "Please enter a value");
}
if (Wrapper.NumberBoxUsage1 == NumberBoxUsages.UvbPriPct)
{
}
return ValidationResult.ValidResult;
}
public Wrapper Wrapper { get; set; }
}
ConverterBindableParameter
public class ConverterBindableParameter : MarkupExtension
{
#region Public Properties
public Binding Binding { get; set; }
public IValueConverter Converter { get; set; }
public Binding ConverterParameterBinding { get; set; }
#endregion
#region Overridden Methods
public override object ProvideValue(IServiceProvider serviceProvider)
{
var multiBinding = new MultiBinding();
multiBinding.Bindings.Add(Binding);
multiBinding.Bindings.Add(ConverterParameterBinding);
var adapter = new MultiValueConverterAdapter
{
Converter = Converter
};
multiBinding.Converter = adapter;
return multiBinding.ProvideValue(serviceProvider);
}
[ContentProperty("Converter")]
public class MultiValueConverterAdapter : IMultiValueConverter
{
public IValueConverter Converter { get; set; }
private object lastParameter;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (Converter == null) return values[0]; // Required for VS design-time
if (values.Length > 1) lastParameter = values[1];
return Converter.Convert(values[0], targetType, lastParameter, culture);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
if (Converter == null) return new object[] { value }; // Required for VS design-time
return new object[] { Converter.ConvertBack(value, targetTypes[0], lastParameter, culture) };
}
}
#endregion
}
You are missing the BindingProxy that captures the DataContext:
public class BindingProxy : System.Windows.Freezable
{
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new PropertyMetadata(null));
}
XAML:
<TextBox.Resources>
<local:NumberBoxValueConverter x:Key="NumberBoxConverter"/>
<local:BindingProxy x:Key="proxy" Data="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</TextBox.Resources>
...
<local:Wrapper NumberBoxUsage1="{Binding Source={StaticResource proxy}, Path=Data.NumberBoxUsage,
Converter={StaticResource DebugDummyConverter}}" />
Today I found something strange (for me). If I want to update a DataGrid through a property in my ViewModel the Binding wont get notified. The special think here is (I think) the Binding is bound to another object (part of an Collection) not directly the property i change.
I have prepared some sample code for you. But first a little (Depper) explanation.
Of course it is just a sample but here I have a ViewModel with two public Properties (Items and CurrentItem). Items is a ObservableCollection and serves as the ItemsSource of my DataGrid. CurrentItem is a String which serves as indicator for a converter to set the background colour (for the grid).
I add two instances o String to my Collection and after the program is started the behaviour is as expected. The first line is green the second is white (set through the converter and the properties).
But if I change the value of CurrentItem after the program was loaded (lets say through the button) the colours wont update on my Datagrid.
If I create a breakpoint at the beginning of the converter I can see (after the loading process) the converter wont execute again so it has to be a Problem with the Binding. I think the problem is my property which is not part of the items in my Collection. The OnPropertyChange method seems to not trigger the update for the RowStyle properly.
In real life the model class of the collections is not a string and the model class implements INotifyPropertyChange (but I don´t think this is the problem cause i just don´t update anything in the model).
I need this kind of behaviour to visible highlight more rows based on a dynamic indicator (similar to the example). If no one knows a better way I think I will implement some kind of Property in my models and update the property with a method from the ViewModel.
ViewModel:
public class MainWindowViewModel : INotifyPropertyChanged
{
public static MainWindowViewModel instance = null;
private string _CurrentItem;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string Property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(Property));
}
}
public string CurrentItem
{
get
{
return _CurrentItem;
}
set
{
if (value != _CurrentItem)
{
_CurrentItem = value;
OnPropertyChanged("CurrentItem");
OnPropertyChanged("Items");
}
}
}
public ObservableCollection<String> Items { get; set; }
public MainWindowViewModel()
{
instance = this;
Items = new ObservableCollection<string>();
CurrentItem = "First";
Items.Add("First");
Items.Add("Second");
Items.Add("First");
}
View XAML
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<local:StringToColorConverter x:Key="StringToColorConverter" />
</Window.Resources>
<DockPanel Margin="30">
<Button DockPanel.Dock="bottom" Content="From First to Second" Click="Button_Click" />
<DataGrid IsReadOnly="True" ItemsSource="{Binding Items}" ColumnWidth="*" AutoGenerateColumns="False">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background"
Value="{Binding Converter={StaticResource StringToColorConverter}}" />
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Text" Binding="{Binding}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
Converter
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var item = value as string;
if (item == MainWindowViewModel.instance?.CurrentItem)
return "Green";
return "White";
}
So sorry for the long post I hope you can comprehend my problem and of course maybe help me :)
You can involve CurrentItem using an IMultiValueConverter
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background">
<Setter.Value>
<MultiBinding Converter="{StaticResource MultiStringToColorConverter}">
<Binding />
<Binding Path="DataContext.CurrentItem"
RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type Window}}"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGrid.RowStyle>
The Converter
public class MultiStringToColorConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
var item = values[0] as string;
var current = values[1] as string;
if (item == current)
return new SolidColorBrush(Colors.Green);
return new SolidColorBrush(Colors.White);
}
public object[] ConvertBack(object values, Type[] targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Everything is normal !
It is by design.
Your background is bound to the n-th object of the DataGrid.
It is not bound to CurrentItem, so there is no reason the binding updates the n-th line background.
Because you have an ObservableCollection, you could put a IsSelected property in MyItem class
And you should make MyItem ràise a PropertyChanged event on IsSelected property.
Of course MyItem would implement INotifyPropertyChanged
Last, you should change the binding :
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background"
Value="{Binding Path=IsSelected,
Converter={StaticResource BooleanToColorConverter}}" />
</Style>
</DataGrid.RowStyle>
Of course changing the StringToColorConverter into BooleanToColorConvert wouldbe trivial.
Regards
I have got a view model with a property:
public class MyModel
{
public bool IsEnabled {get;set;}
}
I want to use this property to toggle a button state. If the boolean is true I want to hide the button, and otherwise show it.
I tried things like:
<Button Visibility= "{Binding IsEnabled ? Hidden : Visible }">Enable</Button>
But this doesn't fit.
I tried some more complex solution but my guess is, I am missing something trivial.
Any suggestions?
Since you want to toggle between Hidden and Visible and true is hidden you can either write custom IValueConverter or use simple Style.Trigger
<Button Content="Enable">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled}" Value="True">
<Setter Property="Visibility" Value="Hidden"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
This is all assuming the DataContext is set accordingly and MyModel.IsEnabled raises INotifyPropertyChanged.PropertyChanged event whenever changed
public class MyModel : INotifyPropertyChanged
{
private bool _isEnabled;
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
OnPropertyChanged("IsEnabled");
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
Use the BooleanToVisibilityConverter:
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
<Button Visibility= "{Binding IsEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" />
Add a class inheriting IValueConverter
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool bValue = (bool)value;
if (bValue)
return Visibility.Visible;
else
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Visibility visibility = (Visibility)value;
if (visibility == Visibility.Visible)
return true;
else
return false;
}
#endregion
}
I have a Style converter defined as below:
public class StyleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is MyStatus && targetType == typeof(Style))
{
var status = (MyStatus)value;
switch (status)
{
case MyStatus.First:
return Application.Current.FindResource("firstStyle");
case MyStatus.Second:
return Application.Current.FindResource("secondStyle");
}
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
In App.xaml I've got some styles defined as below:
<Style x:Key="firstStyle" TargetType="Border">
<Setter Property="Background" Value="Yellow" />
</Style>
<Style x:Key="secondStyle" TargetType="Border">
<Setter Property="Background" Value="LightGreen" />
</Style>
And in Window.xaml:
<MyApp:StyleConverter x:Key="StyleConverter" />
<DataTemplate DataType="{x:Type MyApp:Item}">
<Border x:Name="ItemBorder"
Style="{Binding Path=Status, Mode=OneWay, Converter={StaticResource StyleConverter}}">
<!-- some content here -->
</Border>
</DataTemplate>
<ItemsControl x:Name="MyItems" />
Item:
public class Item : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private MyStatus status;
public MyStatus Status {
get
{
return status;
}
set
{
status = value;
PropertyChanged(this, new PropertyChangedEventArgs("Status"));
}
}
}
I'm adding Item instances to ObservableCollection<Item> collection which is bound to MyItems (and MyStatus is a simple enum)
My problem is that the style is applied properly only for the first time and is not changing after I change the Status property of Item