WPF Expander Validation - c#

Does anyone know of a way to change the style of an expander if a IDataError validation occurs in a control held within the expander. E.g.
<Expander Header="Details">
<TextBox Text="{Binding Brand.DESCRIPTION,
UpdateSourceTrigger=LostFocus,
ValidatesOnDataErrors=True}"/>
</Expander>
So if the textbox has an error the style of my expander will change (go red maybe).
I'm looking to make this as generic as possible so without binding to each control within the expander manually if possible.

You could make use of the Attached Event Validation.Error (which is raised everytime a validation error is added or removed) through an Attached Behavior. To make this work you need to add NotifyOnValidationError=True to the bindings.
This Attached Behavior, ChildValidation, subscribes to the Validation.Error event for the Expander which is bubbled up if NotifyOnValidationError is set to True on the bindings. Since several Controls may be located within the Expander it also need to keep track of the count of Validation Errors that's currently active to determine if a Red Border should be displayed or not. It could look like this
Xaml
<Expander Header="Details"
behaviors:ChildValidationBehavior.ChildValidation="True">
<TextBox Text="{Binding Brand.DESCRIPTION,
UpdateSourceTrigger=LostFocus,
ValidatesOnDataErrors=True,
NotifyOnValidationError=True}"/>
</Expander>
ChildValidationBehavior
public static class ChildValidationBehavior
{
private static readonly DependencyProperty ErrorCountProperty =
DependencyProperty.RegisterAttached("ErrorCount",
typeof(int),
typeof(ChildValidationBehavior));
private static void SetErrorCount(DependencyObject element, int value)
{
element.SetValue(ErrorCountProperty, value);
}
private static int GetErrorCount(DependencyObject element)
{
return (int)element.GetValue(ErrorCountProperty);
}
public static readonly DependencyProperty ChildValidationProperty =
DependencyProperty.RegisterAttached("ChildValidation",
typeof(bool),
typeof(ChildValidationBehavior),
new UIPropertyMetadata(false, OnChildValidationPropertyChanged));
public static bool GetChildValidation(DependencyObject obj)
{
return (bool)obj.GetValue(ChildValidationProperty);
}
public static void SetChildValidation(DependencyObject obj, bool value)
{
obj.SetValue(ChildValidationProperty, value);
}
private static void OnChildValidationPropertyChanged(DependencyObject dpo,
DependencyPropertyChangedEventArgs e)
{
Control control = dpo as Control;
if (control != null)
{
if ((bool)e.NewValue == true)
{
SetErrorCount(control, 0);
Validation.AddErrorHandler(control, Validation_Error);
}
else
{
Validation.RemoveErrorHandler(control, Validation_Error);
}
}
}
private static void Validation_Error(object sender, ValidationErrorEventArgs e)
{
Control control = sender as Control;
if (e.Action == ValidationErrorEventAction.Added)
{
SetErrorCount(control, GetErrorCount(control)+1);
}
else
{
SetErrorCount(control, GetErrorCount(control)-1);
}
int errorCount = GetErrorCount(control);
if (errorCount > 0)
{
control.BorderBrush = Brushes.Red;
}
else
{
control.ClearValue(Control.BorderBrushProperty);
}
}
}

Related

Properly bind DependencyProperty of UserControl in MVVM

I wanted to create a textbox that can search for files and also keeps track of previously used files. So I made a user control with a DependecyProperty that should give me the current text of the textbox and a button. But everytime I try to bind to the DependencyProperty, the property that binds to it remains empty. In short, the control looks like this:
<UserControl
<!-- ... -->
x:Name="PTB">
<AutoSuggestBox x:Name="SearchBox"
Text="{Binding ElementName=PTB, Path=FilePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button Command="{Binding PickFileCommand}" />
</UserControl
I have this simple ViewModel for the user control
public string FilePath
{
get => _filePath;
set => SetProperty(ref _filePath, value);
}
public async Task PickFile()
{
// ...
}
and this code-behind for the user control
public readonly static DependencyProperty FilePathProperty =
DependencyProperty.Register("FilePath", typeof(string), typeof(PathTextBox), new PropertyMetadata("", new PropertyChangedCallback(OnTextChanged)));
public string FilePath
{
get => (string)GetValue(FilePathProperty);
set => SetValue(FilePathProperty, value);
}
private static void OnTextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
if (dependencyObject is PathTextBox ptb && e.NewValue is string s)
{
ptb.SearchBox.Text = s;
ptb.FilePath = s;
}
}
And when I try to use it like this in my MainPage.xaml:
<customcontrols:PathTextBox x:Name="SearchBox"
KeyUp="SearchBox_KeyUp"
FilePath="{Binding ScriptFilePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
and MainPage.xaml.cs
private async void SearchBox_KeyUp(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter)
{
await ViewModel.OpenSqlFile(ViewModel.ScriptFilePath);
}
}
then ViewModel.ScriptFilePath remains empty, even though I did bind to it. I tried a couple of different things with x:Bind etc., but I couldn't find a way to cleanly implement it in MVVM. I'm using the CommunityToolkit.Mvvm library, if that helps. Any ideas?
From your code, I assume that you have the ViewModel in MainPage.xaml.cs. Then you need to add ViewModel to you binding code.
<customcontrols:PathTextBox
x:Name="SearchBox"
KeyUp="SearchBox_KeyUp"
FilePath="{Binding ViewModel.ScriptFilePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
or even better, use x:Bind ViewModel.ScriptFilePath.

How to put a user control in a static layer ontop of all other controls?

I'm developing an autocomplete user control for WPF using XAML and C#.
I would like to have the pop-up for the suggestions to appear above all controls. Currently my pop up is a ListView . That causes problems since whenever I decide to show it the UI must find a place for it and to do so moves all the controls which are below it further down.
How can I avoid this? I assume I must put it in a layer which is above all of the other controls?
I have written "auto-complete" style controls before by using the WPF Popup control, combined with a textbox. If you use Popup it should appear, as you say, in a layer over the top of everything else. Just use Placement of Bottom to align it to the bottom of the textbox.
Here is an example that I wrote a while ago. Basically it is a text box which, as you type pops up a suggestions popup, and as you type more it refines the options down. You could fairly easily change it to support multi-word auto-complete style code editing situations if you wanted that:
XAML:
<Grid>
<TextBox x:Name="textBox"
Text="{Binding Text, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:IntelliSenseUserControl}}}"
KeyUp="textBox_KeyUp"/>
<Popup x:Name="popup"
Placement="Bottom"
PlacementTarget="{Binding ElementName=textBox}"
IsOpen="False"
Width="200"
Height="300">
<ListView x:Name="listView"
ItemsSource="{Binding FilteredItemsSource, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:IntelliSenseUserControl}}}"
SelectionChanged="ListView_Selected"/>
</Popup>
</Grid>
Code-behind:
public partial class IntelliSenseUserControl : UserControl, INotifyPropertyChanged
{
public IntelliSenseUserControl()
{
InitializeComponent();
DependencyPropertyDescriptor prop = DependencyPropertyDescriptor.FromProperty(ItemsSourceProperty, typeof(IntelliSenseUserControl));
prop.AddValueChanged(this, ItemsSourceChanged);
}
private void ItemsSourceChanged(object sender, EventArgs e)
{
FilteredItemsSource = new ListCollectionView((IList)ItemsSource);
FilteredItemsSource.Filter = (arg) => { return arg == null || string.IsNullOrEmpty(textBox.Text) || arg.ToString().Contains(textBox.Text.Trim()); };
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(IntelliSenseUserControl), new FrameworkPropertyMetadata(null) { BindsTwoWayByDefault = true });
public object ItemsSource
{
get { return (object)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(object), typeof(IntelliSenseUserControl), new PropertyMetadata(null));
#region Notified Property - FilteredItemsSource (ListCollectionView)
public ListCollectionView FilteredItemsSource
{
get { return filteredItemsSource; }
set { filteredItemsSource = value; RaisePropertyChanged("FilteredItemsSource"); }
}
private ListCollectionView filteredItemsSource;
#endregion
private void textBox_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Return || e.Key == Key.Enter)
{
popup.IsOpen = false;
}
else
{
popup.IsOpen = true;
FilteredItemsSource.Refresh();
}
}
private void UserControl_LostFocus(object sender, RoutedEventArgs e)
{
popup.IsOpen = false;
}
private void ListView_Selected(object sender, RoutedEventArgs e)
{
if (listView.SelectedItem != null)
{
Text = listView.SelectedItem.ToString().Trim();
}
}
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
If your Window's content container is a Grid, you can simply do something like
<ListBox Grid.RowSpawn="99" Grid.ColumnSpan="99"/>
to "simulate" an absolute position. You then just have to set its position with Margin, HorizontalAlignment and VerticalAlignment so it lays around the desired control.

Focus on a textbox in autocompletebox?

this is my xaml:
<toolkit:AutoCompleteBox Name="signalNameEditor"
ItemsSource="{Binding MySource}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
IsTextCompletionEnabled="True"
FilterMode="StartsWith"
ValueMemberPath="Label"
MinimumPrefixLength="3"
MinimumPopulateDelay="800"
Style="{StaticResource autoCompleteBoxStyle}">
<toolkit:AutoCompleteBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Name="textBlock" Text="{Binding Label}"/>
</StackPanel>
</DataTemplate>
</toolkit:AutoCompleteBox.ItemTemplate>
</toolkit:AutoCompleteBox>
So, how could i get textblock element in my view? I tried this:
var textBlock = signalNameEditor.FindName("textBlock");
but it is wrong. So could you help me with this or redirect me to a proper solution. Thanks in advance.
Thanks for all aswers, that worked
var textBlock = ((StackPanel)signalNameEditor.ItemTemplate.LoadContent()).FindName("textBlock") as TextBlock;
but unfortunately I didn't get the result, that I expected. The question is how to get focus on textbox in autocompletebox, so that when focus is on autocompletebox I could write something there without double clicking.
I thought that I could do something inside my view
public void SetFocus
{
var textBlock = ((StackPanel)signalNameEditor
.ItemTemplate
.LoadContent())
.FindName("textBlock") as TextBlock;
textBlock.Focus();
}
I know that there are a lot of howto examples for setting focus like this one
autocompletebox focus in wpf
but I can't make it work for me. Is there a solution, that I could get without writing AutoCompleteFocusableBox class?
The solution was simplier. Actually i need to set focus on a textbox in a autocompletebox. For this purpose I used style defined as a regular style http://msdn.microsoft.com/ru-ru/library/dd728668(v=vs.95).aspx
After it in my view I could use the following:
public void SetFocus()
{
var textbox = this.editor.Template.FindName("Text", editor) as TextBox;
textbox.Focus();
}
You can Write extension and set custom property for textbox to make it focusable
For example you can write extension class as below
public static class FocusBehavior
{
#region Constants
public static readonly DependencyProperty IsFocusedProperty =
DependencyProperty.RegisterAttached("IsFocused", typeof (bool?),
typeof (FocusBehavior), new FrameworkPropertyMetadata(IsFocusedChanged));
#endregion
#region Public Methods
public static bool GetIsFocused(DependencyObject obj)
{
return (bool) obj.GetValue(IsFocusedProperty);
}
public static void SetIsFocused(DependencyObject obj, bool value)
{
obj.SetValue(IsFocusedProperty, value);
}
#endregion
#region Event Handlers
private static void IsFocusedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uie = (UIElement) d;
if ((bool) e.NewValue)
uie.Dispatcher.BeginInvoke(DispatcherPriority.Input, new ThreadStart(() => Keyboard.Focus(uie)));
}
#endregion Event Handlers
}
Then in xaml as below:
<UserControl xmlns:behaviours="clr-namespace:Example.Views.Behaviours">
<TextBox TextWrapping="Wrap" Text="TextBox" behaviours:FocusBehavior.IsFocused={Binding IsFocused}/>
I hope that answeres your question

Dependency Property in User Control works only on the first instance

I have several custom user controls in a window. They appear dynamically, like workspaces.
I need to add a dependency property on an itemscontrol to trigger a scrolldown when an item is being added to the bound observable collection to my itemscontrol, like so:
(usercontrol)
<ScrollViewer VerticalScrollBarVisibility="Auto" >
<ItemsControl Grid.Row="0" ItemsSource="{Binding Messages}" View:ItemsControlBehavior.ScrollOnNewItem="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox IsReadOnly="True" TextWrapping="Wrap" Text="{Binding Path=DataContext, RelativeSource={RelativeSource Self}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
And the code of my dependency property :
public class ItemsControlBehavior
{
static readonly Dictionary<ItemsControl, Capture> Associations =
new Dictionary<ItemsControl, Capture>();
public static bool GetScrollOnNewItem(DependencyObject obj)
{
return (bool)obj.GetValue(ScrollOnNewItemProperty);
}
public static void SetScrollOnNewItem(DependencyObject obj, bool value)
{
obj.SetValue(ScrollOnNewItemProperty, value);
}
public static readonly DependencyProperty ScrollOnNewItemProperty =
DependencyProperty.RegisterAttached(
"ScrollOnNewItem",
typeof(bool),
typeof(ItemsControl),
new UIPropertyMetadata(false, OnScrollOnNewItemChanged));
public static void OnScrollOnNewItemChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var mycontrol = d as ItemsControl;
if (mycontrol == null) return;
bool newValue = (bool)e.NewValue;
if (newValue)
{
mycontrol.Loaded += new RoutedEventHandler(MyControl_Loaded);
mycontrol.Unloaded += new RoutedEventHandler(MyControl_Unloaded);
}
else
{
mycontrol.Loaded -= MyControl_Loaded;
mycontrol.Unloaded -= MyControl_Unloaded;
if (Associations.ContainsKey(mycontrol))
Associations[mycontrol].Dispose();
}
}
static void MyControl_Unloaded(object sender, RoutedEventArgs e)
{
var mycontrol = (ItemsControl)sender;
Associations[mycontrol].Dispose();
mycontrol.Unloaded -= MyControl_Unloaded;
}
static void MyControl_Loaded(object sender, RoutedEventArgs e)
{
var mycontrol = (ItemsControl)sender;
var incc = mycontrol.Items as INotifyCollectionChanged;
if (incc == null) return;
mycontrol.Loaded -= MyControl_Loaded;
Associations[mycontrol] = new Capture(mycontrol);
}
class Capture : IDisposable
{
public ItemsControl mycontrol{ get; set; }
public INotifyCollectionChanged incc { get; set; }
public Capture(ItemsControl mycontrol)
{
this.mycontrol = mycontrol;
incc = mycontrol.ItemsSource as INotifyCollectionChanged;
incc.CollectionChanged +=incc_CollectionChanged;
}
void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
ScrollViewer sv = mycontrol.Parent as ScrollViewer;
sv.ScrollToBottom();
}
}
public void Dispose()
{
incc.CollectionChanged -= incc_CollectionChanged;
}
}
}
During the first instantiation of my user control, it works like a charm.
But when another user control of the same type is dynamically instantiated, the DependencyProperty is never attached anymore to my scrollviewer. Only the first instance will work correctly.
I know that dependency properties are static, but does that mean they can't work at the same time on several user control of the same type added to the window?
Update 02/03 : Here's how I set the viewmodel to the view (not programmatically) :
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:testDp.ViewModel"
xmlns:View="clr-namespace:testDp.View">
<DataTemplate DataType="{x:Type vm:ChatTabViewModel}">
<View:ChatTabView />
</DataTemplate>
</ResourceDictionary>
even with x:shared = false in the datatemplate tag, it won't work.
But if I set the datacontext in a classic way like usercontrol.datacontext = new viewmodel(), it definitely work. But it's recommended to have a "shared" view, so how do we make dependency properties work with this "xaml" way of setting datacontext ?
Sorry, I couldn't reproduce your problem.
I started Visual C# 2010 Express, created a new 'WPF Application', added your XAML to a UserControl that I imaginatively titled UserControl1, and added your ItemsControlBehavior class. I then modified the MainWindow that VC# created for me as follows:
MainWindow.xaml (contents of <Window> element only):
<StackPanel Orientation="Vertical">
<Button Content="Add user control" Click="ButtonAddUserControl_Click" />
<Button Content="Add message" Click="ButtonAddMessage_Click" />
<StackPanel Orientation="Horizontal" x:Name="sp" Height="300" />
</StackPanel>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public ObservableCollection<string> Messages { get; private set; }
public MainWindow()
{
InitializeComponent();
Messages = new ObservableCollection<string>() { "1", "2", "3", "4" };
DataContext = this;
}
private void ButtonAddUserControl_Click(object sender, RoutedEventArgs e)
{
sp.Children.Add(new UserControl1());
}
private void ButtonAddMessage_Click(object sender, RoutedEventArgs e)
{
Messages.Add((Messages.Count + 1).ToString());
}
}
I made no modifications to the XAML in your UserControl, nor to your ItemsControlBehavior class.
I found that no matter how many user controls were added, their ScrollViewers all scrolled down to the bottom when I clicked the 'Add message' button.
If you're only seeing the scroll-to-the-bottom behaviour on one of your user controls, then there must be something that you're not telling us.

Need help with caliburn Message.Attach when TextBox gets focus

I have a TextBox that I am setting the focus on using an attached property bound to a property of the view model. The attached property calls "UIElement.Focus()" to set the focus. The problem is when the TextBox receives focus in this manner the "GotFocus" event doesn't fire. I am using Caliburn.Micro's Message.Attach to handle the event. Any ideas?
Here is the TextBox.
<TextBox x:Name="Test"
Grid.Column="0"
Text="{Binding Test, Converter={StaticResource TestToStringConverter}}"
AttachedProperties:FocusExtension.IsFocused="{Binding IsTestFocused}"
cal:Message.Attach="[Event GotFocus] = [Action OnGotFocus($eventargs)]; />
Here is the Attached Property (found on SO).
public static class FocusExtension
{
public static bool GetIsFocused(DependencyObject obj)
{
return (bool) obj.GetValue(IsFocusedProperty);
}
public static void SetIsFocused(DependencyObject obj, bool value)
{
obj.SetValue(IsFocusedProperty, value);
}
public static readonly DependencyProperty IsFocusedProperty =
DependencyProperty.RegisterAttached("IsFocused", typeof (bool), typeof (FocusExtension),
new UIPropertyMetadata(false, OnIsFocusedPropertyChanged));
private static void OnIsFocusedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uie = (UIElement)d;
if ((bool)e.NewValue)
{
uie.Focus();
}
}
}
I have tried this myself, and am able to replicate the issue. I'm not quite sure why this happens, it may have something to do with the user control's (i.e. the views) lifecycle. One option could be to extend your attached property so that it invokes a verb on your view model at the point at which it calls uie.Focus().
The name of the verb could be a dependency property on your FocusExtension attached property, and could be set in the view.

Categories