I encountered a problem with the IDataErrorInfo Interface and a wizard I'm currently programming.
The intention of my programm is to ask some Inputs ( usually done with a barcode scanner) and depending on the inputs start a specific sequence.
This is working as intendet. To make sure to catch wrong scans all inputs are check with an event ( OnValueParseFailed) If this event is triggered my current textbox is focused and all text selected:
this.MyWizardViewModel.ValueParseFailed += (s, e) =>
{
switch (e.Parameter)
{
case "ProductionOrder":
this.TextBoxProduction.Focus();
this.TextBoxProduction.SelectAll();
break;
The Interface itself is included this way:
public string this[string name]
{
get
{
string result = null;
if ((name == "ProductionOrder") && (!string.IsNullOrEmpty(this.ProductionOrder)))
{
if (this.System.FirmwareVersion == 0)
result = Lang.Strings.WrongEntry;
}
Its working for the first run. But if the wizard is finished or aborted and run a second time without closing the app, no error message is shown.
The Reset simply returns the app to default values.
public void ResetApplikation()
{
this.System.Clear(); // reset System values
this.ProductionOrder = string.Empty;
this.BmsTypeCode = string.Empty;
this.CellStack1TypeCode = string.Empty;
this.CellClass1 = string.Empty;
this.CellStack2TypeCode = string.Empty;
this.CellClass2 = string.Empty;
this.IsSystemProgrammed = false;
this.IsSystemParameterized = false;
this.MyMachine.Abort(); // reset wizard state
}
While debugging I can see the Interface to be handeled correctly. But no error is displayed.
In XAML the binding is set TwoWay
<TextBox Name="TextBoxProduction" Grid.Row="2" Width="200" Margin="10"
Style="{StaticResource TextBoxNormal}" Loaded="TextBoxProduction_Loaded"
Text="{Binding Path=ProductionOrder, ValidatesOnDataErrors=True,
NotifyOnValidationError=True, Delay=100,
UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
I'm using MahApps but as the textbox class is based on the wpf textbox I doubt a bug in this element is the problem. Any suggestions would be great.
Thank you.
The Answer of Domysee helped me.
Implementing INotifyDataErrorInfo instead of IDataErrorInfo was a major change but it fixed the problem!
Related
I inherited a project that uses the Xamarin Extended Forms controls. All of the text boxes on the form use this. I am trying to do something that feels very simple... I want to display and capture numbers and display them with thousands seperators. So, my odometer field displays 356,098 instead of 356098, much easier to read. It needs to save the integer without formatting in the binding.
I've tried various forms of string formatting on the text box and when my page loads I get an endless loop. It seems like the control formats the field and updates it which triggers the getter/setter which then tries to format it without the comma which triggers the textbox that there is a change, through the binding, and it adds the comma, which triggers another loop and on and on.
Here is the control.
<xfx:XfxEntry
Text="{Binding OdometerEntry, StringFormat='{0:N0}'}"
TextColor="{Binding TextColor}"
IsEnabled="{Binding READ_ONLY, Converter={StaticResource NegateBool}}"
FontSize="Medium"
Keyboard="Numeric"
Margin="10,-20,0,-20"
Grid.Row="2"
Grid.Column="1" />
And here is the model for that field:
public string OdometerEntry
{
get => _odometerEntry;
set
{
if (string.IsNullOrEmpty(value) || value == "0")
{
if (Inspection != null && Inspection.Truck != null)
{
Inspection.Truck.OdometerEntry = 0;
}
_odometerEntry = "";
}
else
{
int i = General.IntParseSafe(value);
if (i < 0)
{
i = 0;
}
if (Inspection != null && Inspection.Truck != null)
{
Inspection.Truck.OdometerEntry = i;
}
_odometerEntry = i.ToString();
}
OnPropertyChanged();
}
}
I tried adding formatting in the ToString but that did not work. I realize this form extender is no longer supported, but I'm hoping someone has some insight.
UPDATE
I tried using the regular Entry tag but same result. The string just will not format.
<Entry Text="{Binding OdometerEntry,StringFormat='{0:#,0}'}"
Rough sketch of the solution, off the top of my head:
int _numericValue;
string _previousString = "";
override OnTextChanged(...)
{
string newText = ...; // From a parameter to OnTextChanged.
if (string.Equals(newText, _previousString))
return; // Avoid infinite loop - this call caused by our change!
string formattedString = StringFormat("...", newText); // Apply desired format.
if (!string.Equals(formattedString, _previousString))
{
_previousString = formattedString;
// CAUTION: This triggers another call to `OnTextChanged`.
theTextField.Text = formattedString;
// TODO: Set cursor to end of text?
}
}
Here is my problem : I created a programm with Fluent ribbon, and when I want to disable a ribbon, I need to use the following code :
Code WPF :
<Fluent:RibbonGroupBox x:Name="GpRibbonFormats" ...>
<Fluent:Button x:Name="AjoutTole" Header="{x:Static p:Resources.Ajouter}">
<Fluent:Button.ToolTip>
<Fluent:ScreenTip x:Name="ScreenTipAjoutTole"...>
</Fluent:ScreenTip>
</Fluent:Button.ToolTip>
</Fluent:Button>
<Fluent:Button x:Name="EditQtyFormat" ...>
<Fluent:Button.ToolTip>
<Fluent:ScreenTip x:Name="ScreenTipEditQtyFormat"...>
</Fluent:ScreenTip>
</Fluent:Button.ToolTip>
</Fluent:Button>
<Fluent:Button x:Name="DeleteFormat" SizeDefinition="Large">
<Fluent:Button.ToolTip>
<Fluent:ScreenTip x:Name="ScreenTipDeleteFormat" ...>
</Fluent:ScreenTip>
</Fluent:Button.ToolTip>
</Fluent:Button>
</Fluent:RibbonGroupBox>
Code Behind :
AjoutTole.IsEnabled = false;
ScreenTipAjoutTole.DisableReason = isBlocked;
EditQtyFormat.IsEnabled = false;
ScreenTipEditQtyFormat.DisableReason = isBlocked;
DeleteFormat.IsEnabled = false;
ScreenTipDeleteFormat.DisableReason = isBlocked;
It works fine but I would like to make a function like that, so I am sure I always send correct information in DisableReason :
DisableButton(Fluent:Button NameOfButton,string ReasonOfDisable)
{
NameOfButton.IsEnabled = false;
NameOfButton.AllScreenTipChild.DisableReason=ReasonOfDisable
}
The same way I would like to disable all a group of buttons :
DisableGroup(Fluent:RibbonGroupBox myGroup,string ReasonOfDisable)
{
foreach(Fluent:Button button in myGroup)
{
button.isEnable=false;
button.AllScreenTipChild.DisableReason=ReasonOfDisable;
}
}
How such a thing is it possible?I want to be able to do it from codebehind.
Edit :
When trying to get the children of my button, I return one element of type System.Windows.Controls.Border, which name is "border", but I don't have such element in my XAML file.
I also tried to get children of my RibbonGroupBox, but in that case I return one grid (grid2), and that grid is not even in the Ribbon...
Code used :
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(DeleteOL); i++)
{
var child = VisualTreeHelper.GetChild(DeleteOL, i);
string monType = child.GetType().ToString();
if(monType== "System.Windows.Controls.Border")
{
System.Windows.Controls.Border bb = (System.Windows.Controls.Border)child;
string name = bb.Name;
}
}
Edit 2 :
I confirm that getChild doesn't work on ribbon(why?), but I could find how to get list of buttons in a group :
foreach(var item in GpRibbonFormats.Items)
{
if(item.GetType().ToString()=="Fluent.Button")
{
Fluent.Button button = (Fluent.Button)item;
button.IsEnabled = false;
}
}
Now I am still looking on how to find a button's ScreenTip
You seem to mix namespace convention from XAML and C#, in C# you don't use : to reference a namespace, you use . separator instead. For example, StackPanel is inside System.Windows.Controls namespace, so you refer to it like this in C# :
System.Windows.Controls.StackPanel stackPanel = new System.Windows.Controls.StackPanel();
I never tried Fluent, but this code should work.
public void DisableGroup(Fluent.RibbonGroupBox ribbonGroup, string reasonOfDisable)
{
foreach (var item in ribbonGroup.Items)
{
if (item is Fluent.Button)
{
DisableButton((Fluent.Button)item, reasonOfDisable);
}
}
}
public void DisableButton(Fluent.Button button, string reasonOfDisable)
{
button.IsEnabled = false;
if (button.ToolTip is Fluent.ScreenTip)
{
Fluent.ScreenTip screenTip = (Fluent.ScreenTip)button.ToolTip;
screenTip.DisableReason = reasonOfDisable;
}
}
To disable an entire group, you call it like this
DisableGroup(GpRibbonFormats, "Ce groupe n'est pas disponible");
To disable only one button, you call it like this
DisableButton(AjoutTole, "Ajouter est désactivé pour le moment");
By the way, Fluent.RibbonGroupBox inherits from ItemsControl, this control has its own IsEnabled property, you can probably disable an entire group by just setting the property to false (I've not tested it though), but you'll have to go through each button to set their screentip anyway.
GpRibbonFormats.IsEnabled = false;
For this kind of thing, Binding are very powerful in WPF, you might want to read a bit on MVVM. It's not easy to implement at first, but once you get the hang of it, it's a game changer and really simplifies your code and logic.
It took time to me, but I finally understood what users were trying to explain to me (it is not obvious for somebody starting with MVVM, that's why I write it here).
I believed I could easily set my properties IsEnabled to true or false in the code(As in Roger Leblanc answer), then continue binding my ViewModel.
It is not so, as when I set my IsEnable (to true) Property, it replaces IsEnabled="{Binding EnableEditNesting}" by IsEnabled=true, so after that no more binding is done(tell me if I am wrong).
On the end I did the following :
For the GroupBox that don't need different behaviour for each button, I just put a binding on its IsEnable parameter.
<Fluent:RibbonGroupBox x:Name="GpRibbonFormats" IsEnabled="{Binding EnableGpRibbonFormats}" Header="{x:Static p:Resources.Stock}">
<Fluent:RibbonGroupBox.ToolTip>
<Fluent:ScreenTip x:Name="ScreenTipGpRibbonFormats" Image="img\image_engrenage.png" Width="250" Text="{x:Static p:Resources.NestingSendToProduction}" DisableReason="{Binding EnableGpRibbonFormatsReason}">
</Fluent:ScreenTip>
</Fluent:RibbonGroupBox.ToolTip>
<Fluent:Button x:Name="AjoutTole" SizeDefinition="Large" LargeIcon="img\image_add.png" Header="{x:Static p:Resources.Ajouter}" Click="Add_ToleOL_Click">
</Fluent:Button>
...
</Fluent:RibbonGroupBox>
For the GrouBox where I need specific behaviour on each button, I put a Binding for each of the buttons(nothing on the group), and when I need to disable all the group, I then disable buttons one by one.
<Fluent:RibbonGroupBox x:Name="GpRibbonOL" Header="{x:Static p:Resources.NestingLaunchingOrder}">
<Fluent:Button x:Name="DeleteOL" IsEnabled="{Binding EnableDeleteOL}" SizeDefinition="Large" LargeIcon="img\image_delete.png" Header="{x:Static p:Resources.Supprimer}" Click="Supprimer_OF">
<Fluent:Button.ToolTip>
<Fluent:ScreenTip x:Name="ScreenTipDeleteOL" Image="img\image_delete.png" Title="Delete OL" Width="250" Text="Delete element" DisableReason="{Binding EnableEditNestingReason}">
</Fluent:ScreenTip>
</Fluent:Button.ToolTip>
</Fluent:Button>
...
</Fluent:RibbonGroupBox>
ViewModel looks like that, so when I want to Enable/Disable, I just change the tooltip :
private bool enableGpRibbonNesting;
public bool EnableGpRibbonNesting
{
get { return enableGpRibbonNesting; }
set
{
enableGpRibbonNesting = value;
this.NotifyPropertyChanged("EnableGpRibbonNesting");
}
}
private string enableGpRibbonNestingReason;
public string EnableGpRibbonNestingReason
{
get { return enableGpRibbonNestingReason; }
set
{
enableGpRibbonNestingReason = value;
if (value == "")
{
EnableGpRibbonNesting = true;
}
else
{
EnableGpRibbonNesting = false;
}
this.NotifyPropertyChanged("EnableGpRibbonNestingReason");
}
}
I have a ListView with a ItemSource data binding and a SelectedItem data binding.
The ListView is populated with a new ItemSource every time I press the Next or Previous button.
The SelectedItem is updated accordingly, the items in the ItemSource have the Selected state, so it can be remembered when the user navigates back and forth.
While debugging, everything seems to work perfectly. The VM updates the controls as expected, and I can also see that the ListView has the correct selected value when I navigate with the next and previous buttons.
The problem is, that regardless of the fact that the ListView has a correct SelectedItem, the ListView does not visualize the SelectedItem as highlighted.
XAML:
<ListView
x:Name="_matchingTvShowsFromOnlineDatabaseListView"
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="3"
ItemsSource="{Binding AvailableMatchingTvShows}"
SelectedItem="{Binding AcceptedMatchingTvShow, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Behaviour in ViewModel responsible for repopulating the ItemSource and the SelectedItem:
private void UpdateForCurrentVisibleTvShow()
{
var selectedTvShow = FoundTvShows[CurrentTvShow];
// Update the available matches
var availableMatchingTvShows = new ObservableCollection<IWebApiTvShow>();
if (AvailableTvShowMatches[selectedTvShow] != null)
{
foreach (var webApiTvShow in AvailableTvShowMatches[selectedTvShow])
{
availableMatchingTvShows.Add(webApiTvShow);
}
}
AvailableMatchingTvShows = availableMatchingTvShows;
// Update the selected item
AcceptedMatchingTvShow = availableMatchingTvShows.FirstOrDefault(webApiTvShow => webApiTvShow.Accepted);
// Update the progress text
CurrentTvShowInfoText = string.Format(
"TV Show: {0} ({1} of {2} TV Shows)",
FoundTvShows[CurrentTvShow],
CurrentTvShow + 1,
FoundTvShows.Count);
// Update the AcceptedMatchingTvShow selection in the listview
OnPropertyChanged("AcceptedMatchingTvShow");
}
The implementation of AcceptedMatchingTvShow:
public IWebApiTvShow AcceptedMatchingTvShow
{
get
{
IWebApiTvShow acceptedTvShow = null;
if (FoundTvShows.Count > 0)
{
var tvShowName = FoundTvShows[CurrentTvShow];
acceptedTvShow = AvailableTvShowMatches[tvShowName].FirstOrDefault(webApiTvShow => webApiTvShow.Accepted);
}
return acceptedTvShow;
}
set
{
if (value != null)
{
var tvShowName = FoundTvShows[CurrentTvShow];
var currentlyAcceptedTvShow =
AvailableTvShowMatches[tvShowName].FirstOrDefault(webApiTvShow => webApiTvShow.Accepted);
if (currentlyAcceptedTvShow != null)
{
currentlyAcceptedTvShow.Accepted = false;
}
value.Accepted = true;
}
OnPropertyChanged();
}
}
I hope somebody can point me in the right direction. Just to be clear, the ListView has the correct items, and the SelectedItem is set with the correct item.
Well, I found 'a solution' to the problem after a lot of debugging and digging. I would REALLY like to understand if this is how WPF meant the control to behave, or if this is a bug in the ListViews data binding part. If anyone could tell me that, I am very very curious to the correct answer (and maybe I solved this problem in the wrong way, and somebody could explain me how I should've done this).
Anyway, the problem seems to be resolved when I create a copy of the object:
public IWebApiTvShow AcceptedMatchingTvShow
{
get
{
IWebApiTvShow acceptedTvShow = null;
if (FoundTvShows.Count > CurrentTvShow)
{
var tvShowName = FoundTvShows[CurrentTvShow];
acceptedTvShow = AvailableTvShowMatches[tvShowName].FirstOrDefault(webApiTvShow => webApiTvShow.Accepted);
}
if (acceptedTvShow != null)
{
// I MUST create a new instance of the original object for the ListView to update the selected item (why??)
return new WebApiTvShow(acceptedTvShow);
}
return null;
}
set
{
if (value != null)
{
var tvShowName = FoundTvShows[CurrentTvShow];
var availableTvShowMatch = AvailableTvShowMatches[tvShowName];
var currentlyAcceptedTvShow = availableTvShowMatch.FirstOrDefault(webApiTvShow => webApiTvShow.Accepted);
if (currentlyAcceptedTvShow != null)
{
currentlyAcceptedTvShow.Accepted = false;
}
value.Accepted = true;
}
OnPropertyChanged();
}
}
Note the call to the copy constructor :
return new WebApiTvShow(acceptedTvShow);
It works, but seems really ridiculous and smells like a bug in ListView to me. Is it?
I tried to explain the same problem in a simpler example here, if anybody can confirm the bug or can explain me how this should've been implemented I would greatly appreciate the insights.
A bit late to the game, but I had been jumping through hoops to solve this Problem in a similar setup. Setting the SelectedItem in a ListView using a bound Property in the Viewmodel or similar using a bound SelectedIndex just would not work. Until I tried to do it async:
Task.Factory.StartNew(() =>
{
BoundSelectedIndex = index;
});
Seems to work - more advanced contributors may answer why...
i know this is an old post but what worked is overriding the Equals and GetHashCode on your SelectedItem object so the listview can compare the SelectedItem with the bound collection
I have a DateTimePicker as follows:
<UserControl
...
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
...
>
<xctk:DateTimePicker Name="MyDatePicker"
Value="{Binding MyDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
Format="Custom" FormatString="dd/MM/yyyy HH:mm:ss"
AutoCloseCalendar="True"/>
I'm using IDateErrorInfo on my data model to handle business logic errors; for example:
public class MyViewModel : IDataErrorInfo
{
public string Error
{
get { return null; }
}
public string this[string columnName]
{
get
{
string error = DataValid();
CanExecute = (error == string.Empty);
return error;
}
}
CanExecute is a property which manages whether the user can select to submit the data. This all works well, however, if I simply select the date and mash the keyboard (type random letters), the date is reset to 01/01/01. What I would like to happen is for the date to effectively remain unchanged (that is, as it was before I mashed the keyboard). However, I can't seem to find a place to handle the casting error which obviously is occurring when this happens.
How can I trap this?
(The DateTimePicker control is part of the WPF Extension Kit)
If you don't like the way the control handles errors you can handle errors yourself in a subclass, an example of this in my old question Wpf Datepicker Input modification
I've been working on an application in MVVM Light lately. I have a TextBox in my XAML bound to my UI. I'd like to validate any input and ensure that only numbers are entered. I've tried the following code:
My TextBox:
<TextBox TabIndex="1" Height="23" MinWidth="410" DockPanel.Dock="Left"
HorizontalAlignment="Left"
Text="{Binding Input, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding IsEnabled}"
AcceptsReturn="False"
local:FocusExtension.IsFocused="{Binding IsFocused}">
And in my ViewModel:
private string input;
public string Input
{
get { return this.input; }
set
{
decimal test;
if(decimal.TryParse(value, out test))
{
this.input = value;
}
else
{
this.input = "";
}
RaisePropertyChanged("Input");
}
}
This fails to update the UI. If I enter "B" and check the debugger, it runs through the setter, but fails to actually update the UI.
Curiously, if I set this.input = "TEST"; in the else block, the UI updates, but, if I attempt to set it to "", string.Empty, or the value of input before the validation, the UI fails to update.
Is this by design? Possibly a bug? Is there something I'm doing wrong?
Edit I mistakenly forgot to include RaisePropertyChanged in my example code. I've updated it. Raising it isn't the problem as I've watched the debugger run all the way through raising it and returning input via the getter.
Way you use strign type property and then convert to decimal, easier to change lik this:
public decimal Input
{
get { return this.input; }
set
{
this.input = value;
RaisePropertyChanged("Input");
}
}
And for validate use IDataErrorInfo (read more: http://blogs.msdn.com/b/wpfsdk/archive/2007/10/02/data-validation-in-3-5.aspx)
What we have done is created a Custom Control, since we use it for a Currency Text Box. I warn you I have no validation that this is a good idea, or falls in line with MVVM model because all manipulation of the control are done in code behind.
In the control on the textbox we have an event on PreviewTextInput that does this
e.Handled = Functions.DoubleConverter(Convert.ToChar(e.Text), ((TextBox)sender).Text.Replace("$", ""));
Then for the function (which isnt perfect, I have a few issues with it still) is:
static public bool DoubleConverter(char c, string str)
{
if (!char.IsDigit(c))
{
if (c == '.' && (str.Contains('.')))
{
return true;
}
else if (c != '.')
{
return true;
}
}
return false;
}
Please use this as a reference, not exactly as is because it is a very crude implementation.