I'm attempting to catch errors using IDataErrorInfo, but changes in the bound data are not firing IdataErrorInfo.this[]. I believe it's due to the way I'm binding data to the textbox.
My textbox Text is bound to a source as follows:
<TextBox Grid.Row="0" Grid.Column="1" Margin="8 0 0 0"
Text="{Binding LimitsConfiguration.ThisItemMax, Mode=TwoWay, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
Style="{StaticResource ValidatableTextBoxStyle}"
HorizontalAlignment="Left" Width="40" Height="25" VerticalAlignment="Bottom" />
The StaticResource is defined by:
<Style x:Key="ValidatableTextBoxStyle" TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors).CurrentItem.ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
The source is defined as follows. Note that his object is not created in the same namespace and has about 9 different string members defined in it.
public LimitsConfig LimitsConfiguration
{
get { return _limitsConfiguration; }
set
{
_limitsConfiguration = value;
OnPropertyChanged("LimitsConfiguration");
}
}
And my IDataErrorInfo.this[] implementation is as follows:
string IDataErrorInfo.this[string propertyName]
{
get
{
string result = String.Empty;
string limitsErrorMsg = "Enter a numeric value for ";
int i;
if (propertyName == "LimitsConfiguration")
{
if (propertyName == LimitsConfiguration.ThisItemMax.ToString())
{
string msg = limitsErrorMsg + "Max Itmes";
string field = LimitsConfiguration.ThisItemMax.ToString();
result = ValidateLimit(field, msg);
}
}
return result;
}
}
The implementation of IDataInfo.this[string propertyName] is never hit. However, the IDataErrorInfo implementation works when textbox text is bound to a string type. Therefore, I believe the issue is due to the binding of a member of the source (Binding LimitsConfiguration.WaypointsMax), but I'm not sure how to get around it other than create public memebers for all items in the LimitsConfiguraton object (which I'd rather not do).
I am new to WPF, so any ideas would be appreciated.
The text box does not listen for property changed events on the class that contains the LimitsConfiguration property. Instead it listens on the property changed events of the LimitsConfig class, because this is the class that contains the property that is bound to the text box.
Related
Just out of interest....
In case I have a ViewModel with an uninitialized string, which is bound to a Textbox, I can use TargetNullValue to display a default value.
However, I was wondering if I can use the same value to update the string in case it is null?
Basically instead of
set
{
if(value != null) text = value;
else value = "defaultstring";
OnPropertyChanged();
}
just do the same thing from the databinding using TargetNullValue.
You can manipulate the getter as well as the data binding will use the get():
private string text;
public string Text
{
get
{
if (text== null)
return "default value";
else
return this.text;
}
set { this.text= value; }
}
However, if you want to do it in Pure XAML you can use a DataTrigger for this:
<TextBlock Text="{Binding MyText}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock }">
<Style.Triggers>
<DataTrigger Binding="{Binding MyText}" Value="{x:Null}">
<Setter Property="Text" Value="DefaultValue"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
My issue is the behavior of the validation errors in the DataGrid. It validates against my model object's property and displays the correct message, but the validation disappears, along with the original value whenever I select a different row.
In the example .gif below, I remove the name (backspace), hit enter (get the validation message), then click to a different row. Whenever the selected row changes I would expect either a) the validation error to remain OR b) the original value to return, but the row stays blank and the validation error is gone until I double click the row. Once I double click, the original value returns.
I would prefer the validation error to persist, but I'll take either at this point.
Here is the datagrid textblock style:
<Style x:Key="datagridElemStyle" TargetType="{x:Type TextBlock}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Yellow" />
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
Here is the actual DataGridTextColumn:
<DataGridTextColumn Header="NAME"
Width="300"
Binding="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=LostFocus,
ValidatesOnExceptions=True}"
ElementStyle="{StaticResource datagridElemStyle}"
CanUserReorder="False" />
This is my ViewModel wrapper object (irrelevant parts omitted):
public class PointVM : INotifyPropertyChanged
{
public Point DataContext { get; set; }
public string Name
{
get { return DataContext.Name; }
set
{
if (value != DataContext.Name)
{
DataContext.Name = value;
OnPropertyChanged(new PropertyChangedEventArgs("Name"));
}
}
}
And finally, here is my Model (irrelevant parts omitted):
public abstract class Point
{
private string _name;
public string Name
{
get { return _name; }
set
{
string trimmedVal = value.Trim();
#region Validation
if (string.IsNullOrEmpty(trimmedVal))
throw new Exception("Name cannot be empty.");
if (Regex.IsMatch(trimmedVal, #"[^A-Za-z0-9\-_ ]$"))
throw new Exception("Invalid character in name.");
if (trimmedVal.Length > 64)
throw new Exception("Name is too long.");
if ((from p in PointList
where p.Name.Equals(trimmedVal, StringComparison.OrdinalIgnoreCase)
select p).Count() > 0)
throw new Exception("Name is already used.");
#endregion
_name = trimmedVal;
}
}
Thanks for your time.
DataGrid validations can be super annoying. When a DataGridTextColumn begins editing, the actual value of the binding is restored, which is why you see a revert when you enter edit mode.
You basically need to stop that call by handling the BeginningEdit event for the DataGrid.
<DataGrid AutoGenerateColumns="False" BeginningEdit="dg_BeginningEdit">
Code-Behind
private void dg_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
e.EditingEventArgs.Handled = true;
}
FYI: Your binding updates on LostFocus. This solution may not work well if you update on PropertyChanged in the future as it changes the order of operation.
I have five images, when you click one of them I want that one to get full opacity while the other only gets half, to show it is the selected one.
I am using MVVM and generally wondering if I'm doing it the right way
I was thinking about passing the name of the imagesource binded into a property.
<StackLayout Grid.Row="3" Grid.Column="1" Orientation="Horizontal" Spacing="0">
<Image Source="{Binding StatusUnresolved}" HorizontalOptions="Center"
VerticalOptions="Center" HeightRequest="40" Opacity="{Binding StatusUnresolvedOpacity}">
<Image.GestureRecognizers>
<!--<TapGestureRecognizer Command="{Binding Source={x:Reference this}, Path=OnStatusTappedCommand}" CommandParameter="{Binding StatusUnresolved}" />-->
</Image.GestureRecognizers>
</Image>
</StackLayout>
The list that turns the string into status later on.
public List<IssueStatusModel> PossibleStatusValues
{
get
{
var items = new List<IssueStatusModel>
{
new IssueStatusModel("statusUnresolved.png", IssueStatus.Unresolved),
new IssueStatusModel("statusInProgress.png", IssueStatus.InProgress),
new IssueStatusModel("statusDone.png", IssueStatus.Done)
};
return items;
}
}
Property for opacity
public double StatusDoneOpacity
{
get { return statusDoneOpacity; }
set
{
if (statusDoneOpacity != value)
{
statusDoneOpacity = value;
NotifyPropertyChanged(nameof(StatusUnresolvedOpacity));
}
}
}
public string StatusDone
{
get { return "statusDone.png"; }
}
public void OnStatusTapped(string fileName)
{
foreach (IssueStatusModel item in StatusValues)
{
if (item.Name != fileName) continue;
Issue.Status = item.Status;
StatusChecker();
return;
}
}
}
Switch statement Changing all the opacities.
private void StatusChecker()
{
switch (Issue.Status)
{
case IssueStatus.Unresolved:
StatusUnresolvedOpacity = 1;
StatusInProgressOpacity = 0.5;
StatusDoneOpacity = 0.5;
StatusText = "Unresolved";
break;
case IssueStatus.InProgress:
StatusUnresolvedOpacity = 0.5;
StatusInProgressOpacity = 1;
StatusDoneOpacity = 0.5;
StatusText = "In Progress";
break;
case IssueStatus.Done:
StatusUnresolvedOpacity = 0.5;
StatusInProgressOpacity = 0.5;
statusDoneOpacity = 1;
StatusText = "Done";
break;
}
}
The way I'd attack this, if you have multiple images, create an ImageVm and encapsulate any image specific implementation details i.e. enum State and an IsSelected notification properties. Of course if you only have 1 image this becomes trivially easy and you don't need vms
Use a DataTrigger that binds to an IsSelected MVVM property to set the Opacity and state if you need to change the image source. Obviously on click you will need to set the IsSelected Property and deselect the other VMs
Example of DataTrigger for IsSelected
<Image Grid.Column="2" Stretch="None">
<Image.Style>
<Style TargetType="{x:Type Image}">
<Setter Property="Opacity" Value="0.5" />
<Style.Triggers>
<DataTrigger Value="True" Binding="{Binding IsSelected}">
<Setter Property="Opacity" Value="0.5"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
Update
You CAN use triggers with enums, and you can use a tap recognizers to fire commands in your main viewmodals. also commands can take parameters as well.
It's probably better (knowing what you have described in the comments) to just make a State and Severity enum and bind to it, and set the State and Severity via a command by a gesture.
Then you could just make a Trigger for each Image to change the Opacity for each image on the various state and severity.
In my wpf application I have a ComboBox which I want to have the ability to disable the selection of items in the drop-down programmatically. The issue that I am having is that the binding ComboBoxItemIsEnabled is not working as expected inside the setter. If remove the binding and use either True or False it works as expected.
XAML
<ComboBox
ItemsSource="{Binding Path=ConfigItems.Result}"
DisplayMemberPath="Name"
IsEditable="True"
FontSize="14"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
IsTextSearchEnabled="False"
Text="{Binding Path=ConfigItem,
UpdateSourceTrigger=LostFocus,
TargetNullValue={x:Static sys:String.Empty}}"
b:ComboBoxBehaviors.OnButtonPress="True">
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnabled" Value="{Binding ComboBoxItemIsEnabled}" />
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
C#
private string _comboBoxItemIsEnabled = "True";
public string ComboBoxItemIsEnabled
{
get
{
return this._comboBoxItemIsEnabled;
}
set
{
this.SetProperty(ref this._comboBoxItemIsEnabled, value);
}
}
public async Task<ConfigItem[]> LoadConfigItemsAsync(string partialName)
{
try
{
if (partialName.Length >= 5)
{
this.ComboBoxItemIsEnabled = "True";
return await this._Service.GetConfigItemsAsync(partialName);
}
this.ComboBoxItemIsEnabled = "False";
return new[] { new ConfigItem("Minimum of 5 characters required", null)};
}
catch (Exception)
{
this.ComboBoxItemIsEnabled = "False";
return new[] { new ConfigItem("No results found", null) };
}
}
I also get the the following error from the debug console when the ComboBoxIsEnabled is being set.
System.Windows.Data Error: 40 : BindingExpression path error: 'ComboBoxItemIsEnabled' property not found on 'object' ''ConfigItem' (HashCode=56037929)'. BindingExpression:Path=ComboBoxItemIsEnabled; DataItem='ConfigItem' (HashCode=56037929); target element is 'ComboBoxItem' (Name=''); target property is 'IsEnabled' (type 'Boolean')
I am using the same mvvm method to target an IsEnabled property for a button else where without an issue. The only difference I can see in the issue above is that I am setting the property within a setter instead.
Many thanks for any wisdom you can part with on how to solve this issue.
After much procrastinating and smashing my head against the keyboard I managed to come to a solution. As it turns out I needed to set the Relative source for the binding. Because I didn't define the DataContext for my solution, every time I pressed a character in the combobox the ItemSource was updated. This meant the ComboBoxItemIsEnabled binding couldn't be found giving me the error above. Below is my updated code, I have added DataContext in front of my binding and added RelativeSource={RelativeSource AncestorType=ComboBox} behind it.
Below is my final code.
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnabled" Value="{Binding DataContext.ComboBoxItemIsEnabled, RelativeSource={RelativeSource AncestorType=ComboBox}}" />
</Style>
</ComboBox.ItemContainerStyle>
I had a checkbox all column inside the datagrid in WPF C#.
<DataGridCheckBoxColumn Binding="{Binding IsSelected,UpdateSourceTrigger=PropertyChanged}" CanUserSort="False">
<DataGridCheckBoxColumn.ElementStyle>
<Style TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataGridCheckBoxColumn.ElementStyle>
<DataGridCheckBoxColumn.HeaderTemplate>
<DataTemplate x:Name="dtAllChkBx">
<CheckBox Name="cbxAll" HorizontalAlignment="Center" Margin="0,0,5,0" IsEnabled="{Binding Path=DataContext.IsCbxAllEnabled,RelativeSource={RelativeSource AncestorType=DataGrid}}"
IsChecked="{Binding Path=DataContext.AllSelected,RelativeSource={RelativeSource AncestorType=DataGrid},UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridCheckBoxColumn.HeaderTemplate>
</DataGridCheckBoxColumn>
When I check the All checkbox, of course, it will mark all the checkboxes, but once I uncheck one checkbox, the All checkbox is still checked. This should be unchecked. How should I do that using WPF C#.
If I understood you correctly - after any change of IsSelected property inside collection item you should update AllSelected value.
So, you need some callback inside all your items(event or Action or any mechanism you want) and change get logic for AllSelected
Here is some draft for item IsSelected property and constructor:
public bool IsSelected {
get { return isSelected; }
set {
isSelected = value;
OnPropertyChanged();
if (globalUpdate != null) globalUpdate();
}
}
public ItemClass(Action globalUpdate, ...your parameters) {
this.globalUpdate = globalUpdate;
...do smth with your parameters
}
Example of usage:
new ItemClass(() => OnPropertyChanged("AllSelected"))
And of course don't forget about AllSelected getter
public bool AllSelected {
get { return YourGridItemsCollection.All(item => item.IsSelected); }
Now when you check manually all items then AllSelected will be automatically checked, and unchecked when you uncheck any item.