I'm trying to validate a form in a UserControl element that is being used by another UserControl that is inside a Window.
I'm using MVVM pattern and i'm implementing the INotifyDataErrorInfo in the ViewModel of the last UserControl child.
The problem is that, when an error occurs, both, the TextBox inside the UserControl that binds to the property that has generated the error, and the UserControl itself get surrounded by a red box indicating the error, and i want just the TextBox to be highlighted.
Here is the code:
The Window that has the MainView (or the first UserControl):
<Grid>
<pages:MainPage>
<pages:MainPage.DataContext>
<vm:MainViewModel/>
</pages:MainPage.DataContext>
</pages:MainPage>
</Grid>
(It just contains a UserControl as a page)
The UserControl of the "MainPage", that contains the other (and last) UserControl as a page inside a page:
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<DataTemplate DataType="{x:Type vm:SearchViewModel}">
<pages:SearchPage/>
</DataTemplate>
...
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
...
<ContentControl Content="{Binding CurrentPage}"/>
Ok, now beleive me, "CurrentPage" has a ViewModel object taken from a MainViewModel property, so lets suppose that "CurrentPage" is a "SearchViewModel" object, so there we have the SearchPage UserControl.
And now the last UserControl, the SearchPage:
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding CaseNumber}"/>
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding PatientNumber}"/>
<TextBox Grid.Column="1" Grid.Row="2" Text="{Binding PatientName}"/>
<TextBox Grid.Column="1" Grid.Row="3" Text="{Binding PatientFamilyName}"/>
<TextBox Grid.Column="1" Grid.Row="4" Text="{Binding PatientMotherMaidenName}"/>
<TextBox Grid.Column="1" Grid.Row="5" Text="{Binding DoctorName}"/>
Just to make the post as small as possible, i've just added the "form" section of the UserControl.
And now the most important part, the SearchViewModel with the INotifyDataErrorInfo implementation:
public class SearchViewModel : ViewModelBase, INotifyDataErrorInfo, IVMPage
{
private SearchModel searchModel = new SearchModel();
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();
private string patientNumber;
public string PatientNumber
{
get { return patientNumber; }
set
{
int number;
patientNumber = value;
if (int.TryParse(value, out number))
{
searchModel.PatientNumber = number;
ClearErrors("PatientNumber");
}
else
{
AddErrors("PatientNumber", new List<string> { "The value must be a number" });
}
RaisePropertyChanged("PatientNumber");
}
}
private string caseNumber;
public string CaseNumber
{
get { return caseNumber; }
set
{
int number;
caseNumber = value;
if (int.TryParse(value, out number))
{
searchModel.CaseNumber = number;
ClearErrors("CaseNumber");
}
else
{
AddErrors("CaseNumber", new List<string> { "The value must be a number" });
}
RaisePropertyChanged("CaseNumber");
}
}
....
private void ClearErrors(string propertyName)
{
errors.Remove(propertyName);
if (ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
private void AddErrors(string propertyName, List<string> newErrors)
{
errors.Remove(propertyName);
errors.Add(propertyName, newErrors);
if(ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
public System.Collections.IEnumerable GetErrors(string propertyName)
{
if(string.IsNullOrEmpty(propertyName))
{
return errors.Values;
}
else
{
if(errors.ContainsKey(propertyName))
{
return errors[propertyName];
}
else
{
return null;
}
}
}
public bool HasErrors
{
get { return (errors.Count() > 0); }
}
So, the problem is:
For example, if i introduce characters in "CaseNumber" TextBox, it is surrounded with a red line indicating the error AND all the SearchPage UserControl is also surrounded with another red line. What i want is just to mark the TextBox with the red line to indicate the error and NOT all the UserControl.
The curious thing is that, if i comment the sections at AddError and ClearError methods where the ErrorChanged event is fired, the UserControl is no longer surrounded with the red line... But i don't lnow why...
Sorry for the long question and thanks.
Ok, the answer is simple.
The problem was with this line:
<ContentControl Content="{Binding CurrentPage}"/>
Because WPF by default sets the ValidatesOnNotifyDataErrors property to true, when an error happens inside the "CurrentPage" UserControl, the TextBox that generated the error inside the UserControl indicates the error with a red line arround him, as expected, BUT ALSO the ContentControl checks the "GetErrors" method and draws another redline arround all the "CurrentPage" UserControl.
To avoid this and just indicate the error at the TextBox and not all the UserControl, just had to add this to the ContentControl declaration:
<ContentControl Content="{Binding CurrentPage, ValidatesOnNotifyDataErrors=False}"/>
Brief version
Color the background of the text box instead of the border.
(optional) Detailed version
Been there, run into that problem. The border of a textbox is reasonably difficult to change, as so many things play with it. For example, if you are using DevExpress, you have to override the whole textbox style to get at the border, then you start to lose the natural highlighting when the box is selected, etc.
Thus, I suggest coloring the background of the textbox to indicate an error. Its much more obvious to the user, looks great, and works well in practice.
Use a very light red color, this page is good for finding colors that are in harmony with the existing color scheme of your page:
https://color.adobe.com/create/color-wheel/
Related
I have a ListView that I would like to populate in XAML. I'm using a custom DataTemplate to make each ListViewItem added contain a Label and a TextBlock.
The problem is I need to dynamically populate the text of the TextBlock of each ListViewItem with data from a settings property, and I don't know how to create this binding.
Right now I am populating the ListView with an XmlDataProvider, but I can't (or at least can't figure out how to) bind values to the xml data. (I'm not stuck using this method of data population, it's just what I was originally doing when I ran into this problem.)
Basically I need something as follows:
The user enters some data into a text box. That data is saved to user settings. When that happens, the corresponding TextBlock of the ListViewItem in the ListView is updated with the user setting data.
Normally I would bind a TextBlock's text to a user setting as follows:
Text="{Binding Source={x:Static properties:Settings.Default},Path=User_Data_1}"
But how do I do this when the text of the TextBlock is defined in the DataTemplate?
My DataTemplate and XmlDataProvider:
<DataTemplate x:Key="listViewTemplate">
<StackPanel Orientation="Vertical">
<Label x:Name="lblName" Content="{Binding XPath=name}"/>
<TextBlock x:Name="tbValue" Text="{Binding XPath=value}"/>
</StackPanel>
</DataTemplate>
<XmlDataProvider x:Key="PagesData" XPath="Pages">
<x:XData>
<Pages xmlns="">
<page id="page01">
<name>Text file:</name>
<value></value>
<source>Pages/Page_CreateFiles1.xaml</source>
</page>
<page id="page02">
<name>Xml file:</name>
<value></value>
<source>Pages/Page_CreateFiles2.xaml</source>
</page>
<page id="page03">
<name>Memory object database:</name>
<value></value>
<source>Pages/Page_CreateFiles3.xaml</source>
</page>
<page id="page04">
<name>Output database:</name>
<value></value>
<source>Pages/Page_CreateDB.xaml</source>
</page>
</Pages>
</x:XData>
</XmlDataProvider>
My ListView
<ListView x:Name="lvNavigation"
ItemTemplate="{DynamicResource listViewTemplate}"
ItemsSource="{Binding Source={StaticResource PagesData}, XPath=page}"/>
Create a view model with a collection of items
public class Item
{
public string Name { get; set; }
public string Value { get; set; }
}
public class ViewModel
{
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
}
and set the MainWindow's DataContext to an instance of the view model class
public MainWindow()
{
InitializeComponent();
var vm = new ViewModel();
DataContext = vm;
vm.Items.Add(new Item { Name = "Name 1", Value = "Value 1" });
vm.Items.Add(new Item { Name = "Name 2", Value = "Value 2" });
}
Bind to it like this:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Clemmens answer is right, but I just wanted to put it out there that I basically did his approach with a slight modification. I used events to trigger the data change in the listview.
I think it is my own fault though, because I didn't explain my problem well enough. First of all, I wanted to do everything from xaml and I don't think that was possible. Second, I failed to mention that I was using pages in a frame, where the data was coming from the pages and the listview was in my main window that contained the frame. So that's why I ended up using events to communicate between the page and the main window.
So in my main window I've defined my observable collection:
ObservableCollection<NavItem> NavItems = new ObservableCollection<NavItem>();
public MainWindow()
{
InitializeComponent();
NavItems.Add(new NavItem { Name = "Text file:", Value = "", Source = "Pages/Page_CreateFiles.xaml" });
NavItems.Add(new NavItem { Name = "Xml file:", Value = "", Source = "Pages/Page_CreateFiles.xaml" });
NavItems.Add(new NavItem { Name = "Memory object db:", Value = "", Source = "Pages/Page_CreateFiles.xaml" });
NavItems.Add(new NavItem { Name = "Output database:", Value = "", Source = "Pages/Page_CreateDB.xaml" });
lvNavigation.ItemsSource = NavItems;
...
}
"NavItem" is a class that is subscribed to INotifyPropertyChanged. Posting that code will just be a lot, so check out how to do that here: INotifyPropertyChanged
Then in each page I set up an event that I call with the data to send:
public static event EventHandler<NavUpdateMessage> UpdateMessage;
private void OnUpdateMessage(int id, string message)
{
NavUpdateMessage navUpdateMessage = new NavUpdateMessage();
navUpdateMessage.Id = id;
navUpdateMessage.Message = message;
var e = UpdateMessage;
if (e != null)
e(this, navUpdateMessage);
}
With the main window subscribed to that event:
public MainWindow()
{
...
Pages.Page_CreateFiles.UpdateMessage += Pages_UpdateMessage;
Pages.Page_CreateDB.UpdateMessage += Pages_UpdateMessage;
}
private void Pages_UpdateMessage(object sender, NavUpdateMessage e)
{
Dispatcher.BeginInvoke(new Action(() =>
{
NavItems[e.Id].Value = e.Message;
}));
}
I'm sure there's a better, more simple approach to this, but this is what I could figure out. And even though I'm sure no one will see this because this question definitely did not get any traction, please feel free to suggest a better solution so at least I can learn.
I have been doing development work in WPF application which uses an MVVM pattern for a couple of days now. I'm very new to WPF and MVVM pattern as well.
In my scenario, I have a user control view (named EPayView.xaml) which has a textbox that will accept a phone number. The view has a corresponding viewmodel (named EPayViewModel.cs). In the MainWindow.xaml, I have a user control (floating virtual keyboard) which is derived from namespace controls WpfKb.Controls. The MainWindow.xaml also has a corresponding viewmodel (named MainViewModel.cs)
Having said that, I have done research on how to use attached dependency properties which lead me to this solution. Set focus on textbox in WPF from view model (C#) which I believe this is where I could bind the property IsFocused in the textbox of EPayView.xaml.
Below are the codes that I have already incorporated in my solution.
EpayView.xaml (textbox xaml markup)
<TextBox Text="{Binding PhoneNo}" Grid.Row="5" Margin="10,0,10,0" VerticalContentAlignment="Center" FontSize="12" x:Name="Email" behaviors:FocusExtension.IsFocused="{Binding IsFocused, Mode=TwoWay}"/>
MainWindow.xaml (xaml markup)
<Window x:Class="SmartPole540.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:WpfKb.Controls;assembly=SmartPole.WpfKb"
xmlns:wpf="clr-namespace:WebEye.Controls.Wpf;assembly=WebEye.Controls.Wpf.WebCameraControl"
xmlns:utilities="clr-namespace:SoltaLabs.Avalon.Core.Utilities;assembly=SoltaLabs.Avalon.Core"
xmlns:userControls="clr-namespace:SoltaLabs.Avalon.View.Core.UserControls;assembly=SoltaLabs.Avalon.View.Core"
xmlns:square="clr-namespace:SmartPole.View.Square;assembly=SmartPole.View"
xmlns:view="clr-namespace:SmartPole.View;assembly=SmartPole.View"
Title="CitiPulse"
WindowStartupLocation="Manual"
PreviewMouseLeftButtonDown="Window_PreviewMouseLeftButtonDown"
Name="mainWindow">
<userControls:RollPanel.BottomContent>
<square:SquareView Canvas.Top="1010" DataContext="{Binding DataContext.SquareViewModel,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type userControls:RollPanel}}}"/>
</userControls:RollPanel.BottomContent>
<controls:FloatingTouchScreenKeyboard
x:Name="floatKb" Width="500" Height="250" PlacementTarget="{Binding ElementName=MainGrid}"
Placement="Center" AreAnimationsEnabled="False" Visibility="Visible"
IsOpen="{Binding IsChecked, ElementName=kbButton}"/>
</Window>
In the above code, the user control RollPanel.BottomContent host the EPayView.xaml view inside another view which is RollPanel.xaml
EpayViewModel.cs contains the static class FocusExtension for the IsFocused attached property (refer to this solution - Set focus on textbox in WPF from view model (C#)). And, EPayViewModel.cs already implemented INotifyPropertyChanged which is wrapped inside a concrete class ObservableObject that accepts type of T. This is also same with MainViewModel.cs
public class EPayViewModel : ObservableObject<EPayViewModel>, IPaymentViewModel, IActiveViewModel
{ ... }
public class MainViewModel : ObservableObject<MainViewModel>
{ ... }
As such, my goal is that when the textbox in EPayView.xaml has the focus, the floating virtual keyboard (floatKb) in the MainWindow.xaml will be shown.
I'm stuck on how to proceed (I was thinking if a call to FocusExtension static class in EPayViewModel inside my MainViewModel.cs will suffice?), any help is greatly appreciated.
Cheers,
As AnjumSKhan already said, to react to some event in a MVVM way, you'll have to use Command. Command can be called within an EventTrigger, you will need to add a Reference to System.Windows.Interactvity component.
Let's assume you have a simple View and View Model and you need to show this View when the TextBox in a MainWindow got focus.
View (NewWindow.xaml)
<Window x:Class="My.NewWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="NewWindow" Height="300" Width="300">
<TextBlock Text="{Binding Message}"/>
View Model
public class NewWindowViewModel
{
private string _message;
public string Message
{
get { return _message; }
set { _message = value; }
}
}
You also have a MainWindow, it is a main view for an app and it contains the target TextBox. You may see that there is an EventTrigger added to the TextBox and it has a property InvokeCommandAction which is binded to the MainWindowViewModel's command called ShowCommand.
Main Window
<Window x:Class="My.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Interactivity="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" Title="MainWindow" Height="350" Width="525">
<TextBox Height="40" Text="{Binding Text}">
<Interactivity:Interaction.Triggers>
<Interactivity:EventTrigger EventName="GotFocus">
<Interactivity:InvokeCommandAction Command="{Binding ShowCommand}"/>
</Interactivity:EventTrigger>
</Interactivity:Interaction.Triggers>
</TextBox>
In the Show method of MainWindowViewModel NewWindow view is created and got new NewWindowViewModel instance as a DataContext. RelayCommand class is presented in my answer to this question
MainWindowViewModel
public class MainWindowViewModel
{
private string _text;
public string Text
{
get { return _text; }
set { _text = value; }
}
private ICommand _increaseCommand;
public ICommand ShowCommand
{
get
{
if (_increaseCommand == null)
{
_increaseCommand = new RelayCommand(
p => true,
Show);
}
return _increaseCommand;
}
}
private void Show(object obj)
{
var w = new NewWindow();
var nvm = new NewWindowViewModel();
nvm.Message = "Test";
w.DataContext = nvm;
w.Show();
}
}
What is left is to create a new MainWindowViewModel and setup a DataContext for MainWindow.
public MainWindow()
{
InitializeComponent();
var mvm = new MainWindowViewModel();
mvm.Text = "Focus me!";
DataContext = mvm;
}
Hope it will help.
Here is the working code i have: The text and background color property do change when I click the button (but for a micro second) and are then set back to the default text/color. Seems like RaisePropertyChanged is being triggered again and again. Can somebody help point what I am doing wrong?
MainWindow.xaml code
<Window x:Class="BuiltIn_Custom_Commands_Eg.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding txtblck_text, StringFormat=Default: {0}}" Padding="10" FontStyle="Italic" Background="{Binding txtblck_color}"/>
<Button Content="Change Color" Width="100" Height="30" Margin="20" Command="{Binding OkCommand}" />
</StackPanel>
</Grid>
ViewModel Code:
class Example_ViewModel : ViewModelBase
{
#region Properties
private string _txtblck_text;
private Brush _txtblck_color;
public ICommand OkCommand {get; set;}
public string txtblck_text
{
get { return _txtblck_text; }
set
{
_txtblck_text = value;
RaisePropertyChanged("txtblck_text");
}
}
public Brush txtblck_color
{
get { return _txtblck_color; }
set
{
_txtblck_color = value;
RaisePropertyChanged("txtblck_color");
}
}
#endregion
#region Constructor
public Example_ViewModel()
{
OkCommand = new myCommand(myOkExecute, myCanOkExecute);
}
#endregion
private void myOkExecute(object parameter)
{
txtblck_color = Brushes.CadetBlue;
//RaisePropertyChanged("txtblck_color");
txtblck_text = "You Clicked me!!!";
//RaisePropertyChanged("txtblck_text");
}
private bool myCanOkExecute(object parameter)
{
txtblck_color = Brushes.Yellow;
txtblck_text = "You havent clicked me!!!";
return true;
}
}
The CanExecute method will and should be called whenever bindings change. Therefore changing a binding in the Execute method (color) will cause CanExecute to be called again.
Instead, why dont you initialize the colors private member once in the constructor as follows.
public Example_ViewModel()
{
OkCommand = new myCommand(myOkExecute, myCanOkExecute);
_txtblck_color = = Brushes.Yellow;
}
Note, the same is also true for the text property. Normally all property private member should be set up with defaults on initialize (constructor) as this avoids unnecessary calls to INotifyPropertyChanged.
Also, in order to test how the code is behaving and to confirm this just set some breakpoints in the CanExecute method to see how the program flow is behaving.
Your problem is that you shouldn't do any setting of your properties in your myCanOkExecute...because it is that that is being called and changing your properties back to the yellow, etc.
The CanExecute methods of Commands could be called multiple times and sometimes when you don't expect ...e.g. when the focus changes to a different control, when certain controls are being edited/sent keypress, after a Command has been executed, when someone calls CommandManager.InvalidateRequerySuggested, etc.
Thus what's happening is your myCanOkExecute is being called shortly after you have clicked and executed your button.
I have a Textbox in a User Control i'm trying to update from my main application but when I set the textbox.Text property it doesnt display the new value (even though textbos.Text contains the correct data). I am trying to bind my text box to a property to get around this but I dont know how, here is my code -
MainWindow.xaml.cs
outputPanel.Text = outputText;
OutputPanel.xaml
<TextBox x:Name="textbox"
AcceptsReturn="True"
ScrollViewer.VerticalScrollBarVisibility="Visible"
Text="{Binding <!--?????--> }"/> <!-- I want to bind this to the Text Propert in OutputPanel.xmal.cs -->
OutputPanel.xaml.cs
namespace Controls
{
public partial class OutputPanel : UserControl
{
private string text;
public TextBox Textbox
{
get {return textbox;}
}
public string Text
{
get { return text; }
set { text = value; }
}
public OutputPanel()
{
InitializeComponent();
Text = "test";
textbox.Text = Text;
}
}
}
You have to set a DataContext in some parent of the TextBox, for example:
<UserControl Name="panel" DataContext="{Binding ElementName=panel}">...
Then the binding will be:
Text="{Binding Text}"
And you shouldn't need this - referring to specific elements from code behind is usually bad practice:
public TextBox Textbox
{
get {return textbox;}
}
I hope this example will help you.
1) Create UserControl.
2) Add to XAML <TextBlock Text="{Binding Path=DataContext.HeaderText}"></TextBlock>
3) In the code behind of that UserControl add
public partial class MyUserControl: UserControl
{
public string HeaderText { set; get; } // Add this line
public MyUserControl()
{
InitializeComponent();
DataContext = this; // And add this line
}
}
4) Outside of the control and let's say in the MainWindow Load event you have to do like
this.gdMain = new MyUserControl{ HeaderText = "YES" };
If your are starting to bind properties I suggest you check some articles on MVVM.
This is a very powerful architecture you can use on WPF. I found it very useful in my projects.
Check this one.
A note - the classes I have are EntityObject classes!
I have the following class:
public class Foo
{
public Bar Bar { get; set; }
}
public class Bar : IDataErrorInfo
{
public string Name { get; set; }
#region IDataErrorInfo Members
string IDataErrorInfo.Error
{
get { return null; }
}
string IDataErrorInfo.this[string columnName]
{
get
{
if (columnName == "Name")
{
return "Hello error!";
}
Console.WriteLine("Validate: " + columnName);
return null;
}
}
#endregion
}
XAML goes as follows:
<StackPanel Orientation="Horizontal" DataContext="{Binding Foo.Bar}">
<TextBox Text="{Binding Path=Name, ValidatesOnDataErrors=true}"/>
</StackPanel>
I put a breakpoint and a Console.Writeline on the validation there - I get no breaks. The validation is not executed. Can anybody just press me against the place where my error lies?
This may be a silly answer, but by default the binding calls the setter when LostFocus happens. Try doing that if you haven't done that.
If you want the error code to be triggered on every key press, Use UpdateSourceTrigger=PropertyChanged inside the binding.
You forgot to implement INotifyPropertyChanged on the 'Bar' class, then only the binding system will trigger the setter.
So your 'Name' property should most likely be.
public string Name
{
get{ return _name; }
set
{
_name = value;
RaisePropertyChanged("Name"); // Or the call might OnPropertyChanged("Name");
}
}
I am not familiar with the EntityObject class, nor can I find it in the .NET Framework documentation or a quick google search.
Anyway, what you need to do us use NotifyOnValidationError as well:
<TextBox Text="{Binding Path=Name, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"/>
Try setting the Mode=TwoWay on your binding
You should create local window resource containing Bar class reference and use its key to set StackPanel data context property. Also, don't forget to import its namespace in the window or user control.
Your XAML code should be like following:
<Window x:Class="Project.WindowName"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BarNamespace">
<Window.Resources>
<local:Bar x:Key="bar" />
</Window.Resources>
<StackPanel Orientation="Horizontal" DataContext="{StaticResource bar}">
<TextBox Text="{Binding Path=Name, ValidatesOnDataErrors=true}"/>
</StackPanel>
</Window>
You should make the methods implementing the IDataErrorInfo as public.