I have an ObservableCollection which is based on the following class:
public class Data
{
public string Text { get; set; }
public DateTime Date { get; set; }
public bool IsActive { get; set; }
}
This observablecollection is used and binded as ItemSource for ListView. The following is DataTemplate for displaying data -
<DataTemplate>
<ViewCell>
<Frame OutlineColor="White" HasShadow="False">
<!-- Data -->
</Frame>
</ViewCell>
</DataTemplate>
Since the ObservableCollection is a collection of Data class which has a boolean property, I want to use it in order to change the frame's background color:
If property IsActive is true - BackgroundColor is Red
If property IsActive is false - BackgroundColor is Blue
I've looked into implementation of Triggers, however I can't seem to get them working correctly, and I'm not sure what I'm missing.
According to Xamarin Documentation I should be able to do :
<Frame>
<Frame.Trigger>
<!-- -->
</Frame.Trigger>
</Frame>
However that doesn't seem to be possible. Neither is this -
<Frame>
<Frame.Style>
<Style TargetType="Frame">
<Setter Property="BackgroundColor" Value="Blue" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive}" Value="True">
<Setter Property="BackgroundColor" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Frame.Style>
</Frame>
The code above gives the following error message:
Xamarin.Forms.Xaml.XamlParseException: Position 28:26. The Property TargetType is required to create a Xamarin.Forms.DataTrigger object.
Not sure about your trigger problem but I think you should be able to accomplish the color changing by first implementing INotifyPropertyChanged on your Data class like so:
public class Data : INotifyPropertyChanged
{
public string Text { get; set; }
public DateTime Date { get; set; }
private bool _isActive;
public bool IsActive
{
get { return _isActive; }
set
{
if (value == _isActive)
{
return;
}
_isActive = value;
NotifyPropertyChanged("IsActive");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Then, in your xaml, you should be able to do something like this:
<DataTemplate>
<ViewCell>
<Frame Background="{Binding IsActive, Converter={StaticResource IsActiveToColorConverter}}" OutlineColor="White" HasShadow="False">
<!-- Data -->
</Frame>
</ViewCell>
</DataTemplate>
Where IsActiveToColorConverter looks something like:
public class IsActiveToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var isActive = (bool) value;
return isActive ? "Red" : "Blue";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Related
I have a property, bound to a TextBox, which is only allowed to be a positive number (i.e. x > 0). Inspired by this post I have decided to implement this using IDataErrorInfo interface.
Following the instructions in that post, I can get a tool-tip warning to show, if the input cannot be validated. But I would like to have the validation warning shown in a seperate TextBlock, just below the input TextBox.
XAML:
<!-- ValidatingControl Style -->
<Style TargetType="{x:Type FrameworkElement}" x:Key="ValidatingControl">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding
Path=(Validation.Errors)[0].ErrorContent,
RelativeSource={x:Static RelativeSource.Self}}" />
</Trigger>
</Style.Triggers>
</Style>
(...)
<TextBlock Text="Product price:" />
<TextBox
Name="Price"
DataContext="{Binding SelectedProduct}"
Style="{StaticResource ValidatingControl}"
Text="{Binding Path=Price,
StringFormat=N0,
ConverterCulture=da-DK,
Mode=TwoWay,
ValidatesOnDataErrors=True}"/>
<!-- This should contain validation warning -->
<TextBlock Margin="0 0 0 10" />
Binding property (C#):
public class ProductModel : IDataErrorInfo
{
public decimal Price { get; set; }
(...)
// Implementation of IDataErrorInfo
string IDataErrorInfo.Error
{
get { return null; }
}
string IDataErrorInfo.this[string columnName]
{
get
{
if (columnName == "Price")
{
// Validate property and return a string if there is an error
if (Price < 0)
return "Cannot be negative.";
}
// If there's no error, null gets returned
return null;
}
}
}
You can bind your textblock to IDataError interfaces indexer property.
Here modified code
<StackPanel>
<TextBlock Text="Product price:" />
<TextBox Name="Price" Text="{Binding Path=Price, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, ValidatesOnDataErrors=True}"/>
<!-- This should contain validation warning -->
<TextBlock Margin="0 0 0 10" Text="{Binding [Price]}" />
</StackPanel>
Also you need to do some modifications to your view model.
class ViewModel : IDataErrorInfo, INotifyPropertyChanged
{
decimal _price;
public decimal Price
{
get => _price;
set
{
_price = value;
RaisePropertyChanged(nameof(Price));
RaisePropertyChanged("Item[]");
}
}
// Implementation of IDataErrorInfo
string IDataErrorInfo.Error
{
get { return null; }
}
public string this[string columnName]
{
get
{
if (columnName == "Price")
{
// Validate property and return a string if there is an error
if (Price < 0)
return "Cannot be negative.";
}
// If there's no error, null gets returned
return null;
}
}
void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Note, that indexer property for IDataErrorInfo interface is implemented implicitly, otherwise WPF system for some reason can't bind to it.
Also take a look to the INotifyPropertyChanged interface implementation for Price property, especially on the RaisePropertyChanged("Item[]"); line. Without this line WPF's system binding system will not know, if there is an error.
You need to define the indexer as public property, implement the INotifyPropertyChanged interface and raise a change notification for the indexer when the Price property is set. This should work:
public class ProductModel : IDataErrorInfo, INotifyPropertyChanged
{
private decimal _price;
public decimal Price
{
get { return _price; }
set
{
_price = value;
NotifyPropertyChanged();
NotifyPropertyChanged("Item[]");
}
}
string IDataErrorInfo.Error
{
get { return null; }
}
public string this[string columnName]
{
get
{
if (columnName == "Price")
{
// Validate property and return a string if there is an error
if (Price < 0)
return "Cannot be negative.";
}
// If there's no error, null gets returned
return null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<!-- This should contain validation warning -->
<TextBlock DataContext="{Binding SelectedProduct}" Margin="0 0 0 10" Text="{Binding [Price]}" />
I want to bind visibility of a textblock, within a listbox, to a bool value in my ViewModel. Binding works well to a textblock outside the listbox, but it isn't working to the textblock within the listbox. Please help!
xaml code:
<TextBlock x:Name="heading" Visibility="{Binding MyVb.Visible, Converter={StaticResource BoolToVisConverter}}" Width="480"/>
<ListBox x:Name="lstBani1" ItemsSource="{Binding Users}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock x:Name="tb1" Text="{Binding string1}" Visibility="{Binding MyVb.Visible, Converter={StaticResource BoolToVisConverter}}" Width="480"/>
<TextBlock x:Name="tb2" Text="{Binding string2}" Width="480"/>
<TextBlock x:Name="tb3" Text="{Binding string3}" Width="480"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
cs code:
public partial class Page1 : PhoneApplicationPage
{
public Page1()
{
ViewModel model = new ViewModel();
model.Users = GetUsers();
model.MyVb = new MyVisibility();
model.MyVb.Visible = false;
this.DataContext = model;
}
// View Model
public class ViewModel
{
public List<User> Users { get; set; }
public MyVisibility MyVb { get; set; }
}
// Bool to Visibility Converter
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
// Property Classes
public class User
{
public string string1 { get; set; }
public string string2 { get; set; }
public string string3 { get; set; }
}
public class MyVisibility : INotifyPropertyChanged
{
private bool _Visible;
public event PropertyChangedEventHandler PropertyChanged;
public bool Visible
{
get { return _Visible; }
set
{
_Visible = value;
NotifyPropertyChanged("Visible");
}
}
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
private List<Bani> GetUsers() {....}
}
Your ListBox is bound to Users, therefore each item in the list is has a DataContext bound to a User. Therefore your binding is trying to look for the property in the User class, not the parent data context.
Give your page a Name and change your binding to the following:
Visibility="{Binding DataContext.MyVb.Visible, ElementName=yourPageName, Converter={StaticResource BoolToVisConverter}}"
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 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();
In the following DataTemplate, the first binding doesn't work while the 2nd one works, and I would like to know why.
<local:IsEnabledConverter x:Key="isEnabled"/>
<local:Boolean2TextConverter x:Key="txtConverter"/>
<DataTemplate x:Key="fileinfoTemplate" DataType="{x:Type local:MyFileInfo}">
<StackPanel>
<Label x:Name="1stLabel" Content="{Binding Path=Filename}" IsEnabled="{Binding Path=., Converter={StaticResource isEnabled}}"/> <--- doesn't work
<Label x:Name="2ndLabel" Content="{Binding Path=IfPrint, Converter={StaticResource txtConverter}}" IsEnabled="{Binding Path=IsChecked, ElementName=ckBox}"/> <--- works
<CheckBox x:Name="ckBox" IsChecked="{Binding Path=IfPrint}" IsEnabled="{Binding Path=IsValid}" Style="{StaticResource printCkBox}"/>
</StackPanel>
</DataTemplate>
IsEnabledConverter:
class IsEnabledConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
MyFileInfo f = value as MyFileInfo;
return f.IsValid && f.IfPrint;
}
//... omit ConvertBack NotImplementedException stuff
}
Boolean2TextConverter:
class IsEnabledConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
Boolean b = (Boolean)value;
return b.ToString();
}
//similarly omit ConvertBack here
}
Code for MyFileInfo:
public class MyFileInfo {
public string IfPrint {
get;
set;
}
public string IsValid {
get;
set;
}
...
}
Problem: When the CheckBox is toggled, the 2nd Label grays out and shows "false", or becomes normal and shows "true", as it should. However, the first Label doesn't change at all; its IsEnabled state is supposed be the conjunction of two Booleans, one of which is changed by the CheckBox. What is wrong? (note that the IsEnabledConverter is called once upon GUI initialization, but not called again when its binding source changes.)
There are 2 issues here. First you have to implement INotifyPropertyChanged for the ViewModel MyFileInfo. Secondly you have to use MultiBinding here. Because I don't think we have some way to trigger updating the target (such as when toggling the CheckBox) if you bind the whole view model to the IsEnabled target. So here is how it should be done:
Your view model:
public class MyFileInfo : INotifyPropertyChanged {
bool _ifPrint;
bool _isValid;
public bool IfPrint {
get { return _ifPrint; }
set {
if(_ifPrint != value) {
_ifPrint = value;
OnPropertyChanged("IfPrint");
}
}
}
public bool IsValid {
get { return _isValid; }
set {
if(_isValid != value) {
_isValid = value;
OnPropertyChanged("IsValid");
}
}
}
//Implement INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string prop){
var handler = PropertyChanged;
if(handler != null) handler(this, new PropertyChangedEventArgs(prop));
}
//.... should do the same for the remaining properties....
//...
}
Here is the converter used for MultiBinding, which should implement IMultiValueConverter (instead of IValueConverter):
class IsEnabledConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture) {
if(values.Length == 2){
return (bool) values[0] && (bool) values[1];
}
return false;
}
//... omit ConvertBack NotImplementedException stuff
}
Here is the modifed XAML (to use MultiBinding instead):
<Label x:Name="firstLabel" Content="{Binding Path=Filename}">
<Label.IsEnabled>
<MultiBinding Converter="{StaticResource isEnabled}">
<Binding Path="IsValid"/>
<Binding Path="IfPrint"/>
</MultiBinding>
</Label.IsEnabled>
</Label>
Now one of IsValid and IfPrint changing will trigger the MultiBinding's Converter. Here you can also bind to IsChecked of the CheckBox directly instead of indirectly via IfPrint.
PS: Note Name used in XAML (as well as in codebehind) must not start with number.
Since the instance of MyFileInfo does not change while you check/uncheck the checkbox hence IsEnabledConverteris not getting called.
In order to Enable/Disable your 1stLabel depending on two properties, either use MultiValueConverter or use MultiDataTrigger by applying Style to your Label.