I have a question regarding custom attached properties/events. In my scenario I want to attach a property/event to any control. The value of this property/event should be an event handler. In short, it should look like:
<TextBox local:Dragging.OnDrag="OnDrag" />
First I tried to implement OnDrag as an attached property. This works for the case above, but then the following case fails:
<Style TargetType="TextBox">
<Setter Property="local:Dragging.OnDrag" Value="OnDrag" />
</Style>
Because the "OnDrag" string can apparently not be made into a RoutedEventHandler (the attached property's type) by the XAML system.
The next thing I tried then was to try and use an attached event, very much like the builtin Mouse.MouseEnter for example.
The complete code for this is shown at the bottom. There are curious things happening with this version:
If you run the code as shown (with the RegisterRoutedEvent line commented) it will show the "Add handler" function is called. Then the xaml system has an internal exception when applying the style (due to missing registered event I guess).
If you run the code with the RegisterRoutedEvent line in effect everything runs, but the "Add handler" function is never called. I want it to be called though, so that I can register at the drag and drop manager.
Curiously, if I change the event in the EventSetter from my own to Mouse.MouseEnter the code that's automatically generated by the xaml designer (in MainWindow.g[.i].cs) is different.
I am not sure why 2) does not call the AddXYZHandler. MSDN seems to indicate this should work.
Finally my questions:
How can I make this work? Is it possible at all?
Do I better use an attached event or an attached property for my scenario?
in case of properties: How do I fix the Style Setter so it converts the OnDrag string to a proper RoutedEventHandler?
in case of events: What's going wrong here? Any way to fix this? I want AddXYZHandler to be called, but apparently that does not work with the style.
MainWindow.xaml:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GridTest.MainWindow"
xmlns:local="clr-namespace:GridTest"
Title="MainWindow" Height="350" Width="525"
local:XYZTest.XYZ="OnXYZAttached">
<Window.Style>
<Style TargetType="Window">
<EventSetter Event="local:XYZTest.XYZ" Handler="OnXYZStyle" />
</Style>
</Window.Style>
</Window>
MainWindow.xaml.cs:
using System.Windows;
namespace GridTest
{
public class XYZTest
{
//public static readonly RoutedEvent XYZEvent = EventManager.RegisterRoutedEvent("XYZ", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(XYZTest));
public static void AddXYZHandler(DependencyObject element, RoutedEventHandler handler)
{
MessageBox.Show("add handler");
}
public static void RemoveXYZHandler(DependencyObject element, RoutedEventHandler handler)
{
MessageBox.Show("remove handler");
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public void OnXYZAttached(object sender, RoutedEventArgs e)
{
MessageBox.Show("attached");
}
public void OnXYZStyle(object sender, RoutedEventArgs e)
{
MessageBox.Show("style");
}
}
}
}
New code:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GridTest.MainWindow"
x:Name="root"
xmlns:local="clr-namespace:GridTest"
local:XYZTest.ABC="OnXYZTopLevel"
Title="MainWindow" Height="350" Width="525">
<ListBox ItemsSource="{Binding}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Background" Value="Red" />
<Setter Property="local:XYZTest.ABC" Value="OnXYZStyle" />
<!-- <Setter Property="local:XYZTest.ABC" Value="{Binding OnXYZStyleProperty, ElementName=root}" /> -->
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Window>
using System.Windows;
namespace GridTest
{
public class XYZTest
{
public static readonly DependencyProperty ABCProperty = DependencyProperty.RegisterAttached("ABC", typeof(RoutedEventHandler), typeof(XYZTest), new UIPropertyMetadata(null, OnABCChanged));
public static void SetABC(UIElement element, RoutedEventHandler value)
{
System.Diagnostics.Debug.WriteLine("ABC set to " + value.Method.Name);
}
static void OnABCChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("ABC changed to " + ((RoutedEventHandler)e.NewValue).Method.Name);
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new[] { "A", "B", "C" };
}
public void OnXYZTopLevel(object sender, RoutedEventArgs e)
{
MessageBox.Show("handler top level");
}
public void OnXYZStyle(object sender, RoutedEventArgs e)
{
MessageBox.Show("handler style");
}
public RoutedEventHandler OnXYZStyleProperty
{
get { return OnXYZStyle; }
}
}
}
I successfully implemented drag and drop functionality completely using Attached Properties. If I were you, I'd avoid using custom events for this, as you're stuck with their parameters. Personally, I went for ICommand instead, but you could also use delegates.
Please look below at the list of properties and Commands that I used in my drag and drop base class implementation:
/// <summary>
/// Gets or sets the type of the drag and drop object required by the Control that the property is set on.
/// </summary>
public Type DragDropType { get; set; }
/// <summary>
/// Gets or sets the allowable types of objects that can be used in drag and drop operations.
/// </summary>
public List<Type> DragDropTypes { get; set; }
/// <summary>
/// Gets or sets the ICommand instance that will be executed when the user attempts to drop a dragged item onto a valid drop target Control.
/// </summary>
public ICommand DropCommand { get; set; }
/// <summary>
/// Gets or sets the DragDropEffects object that specifies the type of the drag and drop operations allowable on the Control that the property is set on.
/// </summary>
public DragDropEffects DragDropEffects { get; set; }
/// <summary>
/// The Point struct that represents the position on screen that the user initiated the drag and drop procedure.
/// </summary>
protected Point DragStartPosition
{
get { return dragStartPosition; }
set { if (dragStartPosition != value) { dragStartPosition = value; } }
}
/// <summary>
/// The UIElement object that represents the UI element that has the attached Adorner control... usually the top level view.
/// </summary>
protected UIElement AdornedUIElement
{
get { return adornedUIElement; }
set { if (adornedUIElement != value) { adornedUIElement = value; } }
}
The AdornedUIElement property holds an Adorner that displays the dragged items as they are dragged, but is optional for you to implement. In this base class, I have implemented most of the drag and drop functionality and exposed protected abstract methods that derived classes must implement. As an example, this method calls the OnAdornedUIElementPreviewDragOver method to provide derived classes an opportunity to change the behaviour of the base class:
private void AdornedUIElementPreviewDragOver(object sender, DragEventArgs e)
{
PositionAdorner(e.GetPosition(adornedUIElement));
OnAdornedUIElementPreviewDragOver(sender, e); // Call derived classes here <<<
if (e.Handled) return; // to bypass base class behaviour
HitTestResult hitTestResult = VisualTreeHelper.HitTest(adornedUIElement, e.GetPosition(adornedUIElement));
Control controlUnderMouse = hitTestResult.VisualHit.GetParentOfType<Control>();
UpdateDragDropEffects(controlUnderMouse, e);
e.Handled = true;
}
/// <summary>
/// Must be overidden in derived classes to call both the UpdateDropProperties and UpdateDragDropEffects methods to provide feedback for the current drag and drop operation.
/// </summary>
/// <param name="sender">The Control that the user dragged the mouse pointer over.</param>
/// <param name="e">The DragEventArgs object that contains arguments relevant to all drag and drop events.</param>
protected abstract void OnAdornedUIElementPreviewDragOver(object sender, DragEventArgs e);
Then in my extended ListBoxDragDropManager class:
protected override void OnAdornedUIElementPreviewDragOver(object sender, DragEventArgs e)
{
HitTestResult hitTestResult = VisualTreeHelper.HitTest(AdornedUIElement, e.GetPosition(AdornedUIElement));
ListBox listBoxUnderMouse = hitTestResult.VisualHit.GetParentOfType<ListBox>();
if (listBoxUnderMouse != null && listBoxUnderMouse.AllowDrop)
{
UpdateDropProperties(ListBoxProperties.GetDragDropType(listBoxUnderMouse), ListBoxProperties.GetDropCommand(listBoxUnderMouse));
}
UpdateDragDropEffects(listBoxUnderMouse, e);
e.Handled = true; // This bypasses base class behaviour
}
Finally, it is used simply in the UI like so (the RelativeSource declarations and narrow width here make it seem worse than it is):
<ListBox ItemsSource="{Binding Disc.Tracks, IsAsync=True}" SelectedItem="{Binding
Disc.Tracks.CurrentItem}" AllowDrop="True" Attached:ListBoxProperties.
IsDragTarget="True" Attached:ListBoxProperties.DropCommand="{Binding
DataContext.DropTracks, RelativeSource={RelativeSource AncestorType={x:Type
Views:ReleaseTracksView}}}" Attached:ListBoxProperties.DragDropTypes="{Binding
DataContext.DragDropTypes, RelativeSource={RelativeSource AncestorType={x:Type
Views:ReleaseTracksView}}}" Attached:ListBoxProperties.DragEffects="{Binding
DataContext.DragEffects, RelativeSource={RelativeSource AncestorType={x:Type
Views:ReleaseTracksView}}}">
I must be honest though... this was a lot of work. However, now that I can implement drag and drop operations with visual feedback just by setting a few properties, it totally seems worth it.
Related
This question already has answers here:
Clear whitespace from end of string in WPF/XAML
(2 answers)
Closed 1 year ago.
I'm creating a WPF application using MVVM. I'd like to make it so all textboxes in the application, by default trims the text.
I have tried to follow the answer here
I managed to add System.Windows.Interactivty reference via NuGet. I created a UserControl in a behaviors folder and copied the provided code. But Visual Studio cannot find suitable method to override, and AssociatedObject does not exist.
And in the XAML, it does not like <local:TrimTextBoxBehavior /> or xmlns:local="clr-namespace:StaffApp;assembly=mscorlib" or xmlns:local="clr-namespace:StaffApp.Behaviors;assembly=mscorlib"
I have tried a different method of trimming all the binded properties' setters in my Model
e.g. public string MiddleNames { get => _middleNames; set => _middleNames = value.Trim(); }
But I'm not a fan of having to do this for every property, and this causes issues when the textbox is null as defined from my XAML form:
<Label Width="100" Content="Middle name(s)" />
<TextBox Text="{Binding Employee.MiddleNames, TargetNullValue=''}" />
You need a ValueConverter or an attached behavior that you apply via a Style to all TextBox controls. Third option would be to extend the TextBox and override TextBoxBase.OnTextChanged(TextChangedEventArgs).
TextTrimBehavior:
public class TextTrimBehavior : DependencyObject
{
#region IsEnabled attached property
public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
"IsEnabled", typeof(bool), typeof(TextTrimBehavior), new PropertyMetadata(false, TextTrimBehavior.OnAttached));
public static void SetIsEnabled(DependencyObject attachingElement, bool value)
{
attachingElement.SetValue(TextTrimBehavior.IsEnabledProperty, value);
}
public static bool GetIsEnabled(DependencyObject attachingElement)
{
return (bool) attachingElement.GetValue(TextTrimBehavior.IsEnabledProperty);
}
#endregion
private static void OnAttached(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is TextBox attachedTextBox))
{
return;
}
if ((bool) e.NewValue)
{
attachedTextBox.LostFocus += TextTrimBehavior.TrimText;
}
else
{
attachedTextBox.LostFocus -= TextTrimBehavior.TrimText;
}
}
private static void TrimText(object sender, RoutedEventArgs e)
{
if (sender is TextBox textBox)
{
textBox.Text = textBox.Text.Trim();
}
}
}
TextBox Style:
<Style TargetType="TextBox">
<Setter Property="TextTrimBehavior.IsEnabled"
Value="True" />
</Style>
Since the Style has no key it will apply implicitly to all TextBox controls within the scope. To make the style global you have to put it into the App.xaml ResourceDictionary.
Extending the implicit style using Style.BasedOn:
<Style x:Key="ExplicitStyle" TargetType="TextBox"
BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Background"
Value="YellowGreen" />
</Style>
Alternatively you can set the attached property locally
<TextBox TextTrimBehavior.IsEnabled="True"
Text="{Binding Employee.MiddleNames, TargetNullValue=''}" />
You could try using Converters. This way you just have to add the converter to the binding of the textbox and that would do it.
// Property in the View Model
public string Text { get;set; }
// Converter class
public class TrimTextConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (!string.IsNullOrEmpty((string)value)) {
return ((string)value).Trim();
}
return string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return value;
}
}
<!--In the xaml file-->
<!--Reference to the converter namespace-->
xmlns:converter="clr-namespace:namespace-where-converter-is-located"
<!--Adding Converter To Resource Dictionary-->
<ResourceDictionary>
<converter:TrimTextConverter x:Key="TrimTextConverter"/>
</ResourceDictionary>
<!--TextBox-->
<TextBox Grid.Row="4" Text="{Binding Text, Converter={StaticResource TrimTextConverter}">
This is my code to trim text after lost keyboard focus
public class TextBoxTrimBehavior : Behavior<TextBox>
{
/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
/// <remarks>
/// Override this to hook up functionality to the AssociatedObject.
/// </remarks>
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.LostKeyboardFocus += AssociatedObject_LostKeyboardFocus;
}
void AssociatedObject_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
var tb = sender as TextBox;
if (tb != null && tb.Text != tb.Text?.Trim())
{
tb.Text = tb.Text?.Trim();
}
}
/// <summary>
/// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
/// </summary>
/// <remarks>
/// Override this to unhook functionality from the AssociatedObject.
/// </remarks>
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.LostKeyboardFocus -= AssociatedObject_LostKeyboardFocus;
}
}
and use the behavior in xaml as follow
<TextBox>
<i:Interaction.Behaviors>
<behavior:TextBoxTrimBehavior />
</i:Interaction.Behaviors>
</TextBox>
I created a new WPF Project in VS2017 and also imported MVVM Light via NuGet.
Then I added some code that should change the background color from the MainWindows Grid every 25 milliseconds. Sadly that change doesn'T propagate and I have no clue why it doesn't update. Maybe someone here can help me.
Here is the code:
MainViewModel.cs
using GalaSoft.MvvmLight;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace Strober.ViewModel
{
/// <summary>
/// This class contains properties that the main View can data bind to.
/// <para>
/// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel.
/// </para>
/// <para>
/// You can also use Blend to data bind with the tool's support.
/// </para>
/// <para>
/// See http://www.galasoft.ch/mvvm
/// </para>
/// </summary>
public class MainViewModel : ObservableObject
{
private DispatcherTimer timer;
public string Title { get; set; }
private Brush _background;
public Brush Background
{
get
{
return _background;
}
set
{
_background = value;
OnPropertyChanged("Background");
}
}
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel()
{
Background = new SolidColorBrush(Colors.Black);
timer = new DispatcherTimer();
timer.Tick += Timer_Tick;
timer.Interval = new TimeSpan(0, 0, 0,0,100);
timer.Start();
}
private void Timer_Tick(object sender, System.EventArgs e)
{
if (Background == Brushes.Black)
{
Background = new SolidColorBrush(Colors.White);
Title = "White";
}
else
{
Background = new SolidColorBrush(Colors.Black);
Title = "Black";
}
}
#region INotifiedProperty Block
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string PropertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
#endregion
}
}
ViewModelLocator.cs
/*
In App.xaml:
<Application.Resources>
<vm:ViewModelLocator xmlns:vm="clr-namespace:Strober"
x:Key="Locator" />
</Application.Resources>
In the View:
DataContext="{Binding Source={StaticResource Locator}, Path=ViewModelName}"
You can also use Blend to do all this with the tool's support.
See http://www.galasoft.ch/mvvm
*/
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Ioc;
using CommonServiceLocator;
namespace Strober.ViewModel
{
/// <summary>
/// This class contains static references to all the view models in the
/// application and provides an entry point for the bindings.
/// </summary>
public class ViewModelLocator
{
/// <summary>
/// Initializes a new instance of the ViewModelLocator class.
/// </summary>
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
////if (ViewModelBase.IsInDesignModeStatic)
////{
//// // Create design time view services and models
//// SimpleIoc.Default.Register<IDataService, DesignDataService>();
////}
////else
////{
//// // Create run time view services and models
//// SimpleIoc.Default.Register<IDataService, DataService>();
////}
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
public static void Cleanup()
{
// TODO Clear the ViewModels
}
}
}
MainWindow.xaml (MainWindow.xaml.cs is just the regularly generated file)
<Window x:Class="Strober.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Strober"
mc:Ignorable="d"
DataContext="{Binding Main, Source={StaticResource Locator}}"
Title="{Binding Title}" Height="450" Width="800">
<Grid Background="{Binding Background}">
</Grid>
</Window>
App.xaml
<Application x:Class="Strober.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Strober" StartupUri="MainWindow.xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d1p1:Ignorable="d" xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006">
<Application.Resources>
<ResourceDictionary>
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" xmlns:vm="clr-namespace:Strober.ViewModel" />
</ResourceDictionary>
</Application.Resources>
</Application>
greg
The primary issue in your code is that System.Windows.Media.SolidColorBrush does not override the Equals() method, and so your expression Background == Brushes.Black is never true. Since you are creating explicit new instances of the SolidColorBrush object, and since the == operator is just comparing the instance references, the comparison between your brush value and the built-in Brushes.Black instance always fails.
The easiest way to fix the code would be to just use the actual Brushes instances:
private void Timer_Tick(object sender, System.EventArgs e)
{
if (Background == Brushes.Black)
{
Background = Brushes.White;
Title = "White";
}
else
{
Background = Brushes.Black;
Title = "Black";
}
}
Then when you compare the instance references, they are in fact comparable, and you will detect the "black" condition as desired.
I'll note that since you also aren't raising PropertyChanged for changes to the Title property, that binding also will not work as expected.
For what it's worth, I would avoid your design altogether. First, view model objects should avoid using UI-specific types. For sure, this would include the Brush type. Arguably, it also includes the DispatcherTimer, since that exists in service of the UI. Besides which, DispatcherTimer is a relatively imprecise timer, and while it exists primarily to have a timer that raises its Tick event on the dispatcher thread that owns the timer, since WPF automatically marshals property-change events from any other thread to the UI thread, it's not nearly as useful in this example.
Here is a version of your program that IMHO is more in line with typical WPF programming practices:
class MainViewModel : NotifyPropertyChangedBase
{
private string _title;
public string Title
{
get { return _title; }
set { _UpdateField(ref _title, value); }
}
private bool _isBlack;
public bool IsBlack
{
get { return _isBlack; }
set { _UpdateField(ref _isBlack, value, _OnIsBlackChanged); }
}
private void _OnIsBlackChanged(bool obj)
{
Title = IsBlack ? "Black" : "White";
}
public MainViewModel()
{
IsBlack = true;
_ToggleIsBlack(); // fire and forget
}
private async void _ToggleIsBlack()
{
while (true)
{
await Task.Delay(TimeSpan.FromMilliseconds(100));
IsBlack = !IsBlack;
}
}
}
This view model class uses a base class that I use for all my view models, so I don't have to reimplement INotifyPropertyChanged all the time:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdateField<T>(ref T field, T newValue,
Action<T> onChangedCallback = null,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return;
}
T oldValue = field;
field = newValue;
onChangedCallback?.Invoke(oldValue);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
You'll notice that the view model class doesn't have any UI-specific behavior. It would work with any program, WPF or otherwise, as long as that program has a way to react to PropertyChanged events, and to make use of the values in the view model.
To make this work, the XAML gets somewhat more verbose:
<Window x:Class="TestSO55437213TimerBackgroundColor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:p="http://schemas.microsoft.com/netfx/2007/xaml/presentation"
xmlns:l="clr-namespace:TestSO55437213TimerBackgroundColor"
mc:Ignorable="d"
Title="{Binding Title}" Height="450" Width="800">
<Window.DataContext>
<l:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.Style>
<p:Style TargetType="Grid">
<Setter Property="Background" Value="White"/>
<p:Style.Triggers>
<DataTrigger Binding="{Binding IsBlack}" Value="True">
<Setter Property="Background" Value="Black"/>
</DataTrigger>
</p:Style.Triggers>
</p:Style>
</Grid.Style>
</Grid>
</Window>
(Note: I have explicitly named the http://schemas.microsoft.com/netfx/2007/xaml/presentation XML namespace, for use with the <Style/> element, solely as a work-around for Stack Overflow's insufficient XML markup handling, which would not otherwise recognize the <Style/> element as an actual XML element. In your own program, you may feel free to leave that out.)
The key here is that the entire handling of the UI concern is in the UI declaration itself. The view model doesn't need to know how the UI represents the colors black or white. It just toggles a flag. Then the UI monitors that flag, and applies property setters as appropriate to its current value.
Finally, I'll note that for repeatedly-changing state in the UI like this, another approach is to use WPF's animation features. That's beyond the scope of this answer, but I encourage you to read about it. One advantage to doing so is that the animation uses an even higher-resolution timing model than the thread-pool based Task.Delay() method I use above, and so would generally provide an even smoother animation (though, certainly as your interval gets smaller and smaller — such as 25ms as your post indicates you intended to use — you will have trouble getting WPF to keep up smoothly regardless; at some point, you'll find that the higher-level UI frameworks like WinForms, WPF, Xamarin, etc. just can't operate at such a fine-grained timer level).
I have been trying to figure this out with lots of googling and SO, but unfortunately I cannot solve this issue. The more I read, the more confused I get.
I would like to build an autocomplete textbox as a custom control.
My CustomControl:
<UserControl x:Class="ApplicationStyling.Controls.AutoCompleteTextBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ApplicationStyling.Controls"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300"
Name="AutoCompleteBox">
<Grid>
<TextBox Grid.Row="3"
Style="{DynamicResource InputBox}"
x:Name="SearchBox"
Text="{Binding Text}"
TextChanged="{Binding ElementName=AutoCompleteBox, Path=TextChanged}"/>
<ListBox x:Name="SuggestionList"
Visibility="Collapsed"
ItemsSource="{Binding ElementName=AutoCompleteTextBox, Path=SuggestionsSource}"
SelectionChanged="{Binding ElementName=AutoCompleteBox, Path=SelectionChanged}">
<ListBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Label}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
My Code Behind:
using System.Collections;
using System.Windows;
using System.Windows.Controls;
namespace ApplicationStyling.Controls
{
/// <summary>
/// Interaction logic for AutoCompleteTextBox.xaml
/// </summary>
public partial class AutoCompleteTextBox : UserControl
{
public static readonly DependencyProperty SuggestionsSourceProperty;
public static readonly DependencyProperty TextProperty;
// Events
public static readonly RoutedEvent TextChangedProperty;
public static readonly RoutedEvent SelectionChangedProperty;
static AutoCompleteTextBox()
{
// Attributes
AutoCompleteTextBox.SuggestionsSourceProperty = DependencyProperty.Register("SuggestionsSource", typeof(IEnumerable), typeof(AutoCompleteTextBox));
AutoCompleteTextBox.TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(AutoCompleteTextBox));
// Events
AutoCompleteTextBox.TextChangedProperty = EventManager.RegisterRoutedEvent("TextChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AutoCompleteTextBox));
AutoCompleteTextBox.SelectionChangedProperty = EventManager.RegisterRoutedEvent("SelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AutoCompleteTextBox));
}
#region Events
public event RoutedEventHandler TextChanged
{
add { AddHandler(TextChangedProperty, value); }
remove { RemoveHandler(TextChangedProperty, value); }
}
// This method raises the Tap event
void RaiseTextChangedEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(AutoCompleteTextBox.TextChangedProperty);
RaiseEvent(newEventArgs);
}
public event RoutedEventHandler SelectionChanged
{
add { AddHandler(SelectionChangedProperty, value); }
remove { RemoveHandler(SelectionChangedProperty, value); }
}
// This method raises the Tap event
void RaiseSelectionChangedEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(AutoCompleteTextBox.SelectionChangedProperty);
RaiseEvent(newEventArgs);
}
#endregion
#region DProperties
/// <summary>
/// IEnumerable ItemsSource Property for the Suggenstion Box
/// </summary>
public IEnumerable SuggestionsSource
{
get
{
return (IEnumerable)GetValue(AutoCompleteTextBox.SuggestionsSourceProperty);
}
set
{
SetValue(AutoCompleteTextBox.SuggestionsSourceProperty, value);
}
}
/// <summary>
/// This is the Text attribute which routes to the Textbox
/// </summary>
public string Text
{
get
{
return (string)GetValue(AutoCompleteTextBox.TextProperty);
}
set
{
SetValue(AutoCompleteTextBox.TextProperty, value);
}
}
#endregion
public AutoCompleteTextBox()
{
InitializeComponent();
SearchBox.TextChanged += (sender, args) => RaiseTextChangedEvent();
SuggestionList.SelectionChanged += (sender, args) => RaiseSelectionChangedEvent();
}
}
}
And lastly, the way I use it:
<asc:AutoCompleteTextBox x:Name="ShareAutoCompleteBox"
Grid.Row="3"
SelectionChanged="ShareAutoCompleteBox_SelectionChanged"
TextChanged="ShareAutoCompleteBox_TextChanged"/>
where asc is the namespace for the outsourced class library which is loaded via app.xaml.
Anyways, the issues I am getting in the XAML at the TextBox.TextChanged attribute, and when running the code:
System.InvalidCastException: Unable to cast object of type 'System.Reflection.RuntimeEventInfo' to type 'System.Reflection.MethodInfo'.
So what exactly is going on here? I would like to forward the AutoCompleteTextBox TextChanged to the TextBox within the Custom Control Template. Same with the SelectionChanged to the Listbox.
I took most of the code from either https://msdn.microsoft.com/en-us/library/ms752288(v=vs.100).aspx (for the events) and from some other SO questions the code for the custom properties.
Not sure, what the problem is and I am looking forward to your help.
The exception is happening because you are trying to bind the value of the TextChanged field to an attribute that expects a method reference. It's really confusing to WPF. :)
Just remove the TextChanged attribute from the TextBox element in your XAML:
TextChanged="{Binding ElementName=AutoCompleteBox, Path=TextChanged}"
You already subscribe to the event in your constructor, which is enough. If you do want to use the TextChanged attribute instead of subscribing in the constructor, then you can do that, but you need to provide an actual event handler, e.g. a method in the code-behind. That method would just call the RaiseTextChangedEvent() method, just as your current event handler does. It's just that it would be a named method in the class instead of an anonymous method declared in the constructor.
Same thing applies to the other event.
That said, you might reconsider implementing the forwarded events at all. Typically, your control's Text property would be bound to the property of some model object, which can itself react appropriately when that bound property changes. It shouldn't need a separate event on the UserControl object to tell it that its value has changed.
I have a question about databinding!
I am writing code for a 'node editor' that has some (different) nodes in it.
I use a BaseViewModel class that derives from INotifyPropertyChanged.
There is a 'base' NodeViewModel (that derives from it) with an ObservableCollection and other Properties, like the Node's Name property. It's implementation looks like this:
(in public class NodeViewModel : BaseViewModel):
protected String mName = String.Empty;
public String Name {
get { return mName; }
set {
if (mName == value) {
return;
}
mName = value;
OnPropertyChanged("Name");
}
}
With an OnPropertyChanged handler that looks like this:
(in BaseViewModel)
protected virtual void OnPropertyChanged(string propertyName) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Now I have one additional RoomViewModel that derives from NodeViewModel.
I use another different ViewModel that I call RoomCollectionViewModel to group some rooms.
Now when I add a room to my roomcollection (by drawing a connection between them) I test all connected rooms for the same name.
If an already connected room exists in the collection with the same room name (e.g. "new room") I want to change those two room's names to e.g. "new room #1" and "new room #2". No problem so far.
Every node control (created using DataTemplates with set DataContext set to the ViewModel) contains a TextBlock (a modified one) that displays the node's name.
This is where it gets problematic:
I use a modified Textblock because I want to be able to modify the node's name by double-clicking on it. And that works perfectly, only if I modify the RoomViewModel's name in Code, this (modified) TextBlock won't update.
The strange thing is this:
When two equally named rooms in a collection get renamed by my code and I then double-click on the editable TextBlock (which converts to a TextBox in that process), I already see the modified Text. So I assume my DataBinding and my code is correct, just not complete :)
So how is it possible to force an update of my EditableTextBlock, the Text (DependencyProperty) seems to be updated correctly...
I hope you understand what my problem is! Thank you for any help.
Update 1
This is the XAML code for my EditableTextBlock (it comes from here: http://www.codeproject.com/Articles/31592/Editable-TextBlock-in-WPF-for-In-place-Editing)
<UserControl x:Class="NetworkUI.EditableTextBlock"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NetworkUI"
mc:Ignorable="d"
d:DesignHeight="60" d:DesignWidth="240" x:Name="mainControl">
<UserControl.Resources>
<DataTemplate x:Key="EditModeTemplate">
<TextBox KeyDown="TextBox_KeyDown" Loaded="TextBox_Loaded" LostFocus="TextBox_LostFocus"
Text="{Binding ElementName=mainControl, Path=Text, UpdateSourceTrigger=PropertyChanged}"
Margin="0" BorderThickness="1" />
</DataTemplate>
<DataTemplate x:Key="DisplayModeTemplate">
<TextBlock Text="{Binding ElementName=mainControl, Path=FormattedText}" Margin="5,3,5,3" MouseDown="TextBlock_MouseDown" />
</DataTemplate>
<Style TargetType="{x:Type local:EditableTextBlock}">
<Style.Triggers>
<Trigger Property="IsInEditMode" Value="True">
<Setter Property="ContentTemplate" Value="{StaticResource EditModeTemplate}" />
</Trigger>
<Trigger Property="IsInEditMode" Value="False">
<Setter Property="ContentTemplate" Value="{StaticResource DisplayModeTemplate}" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
And here is the code-behind file:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace NetworkUI {
/// <summary>
/// Interaction logic for EditableTextBlock.xaml
/// </summary>
public partial class EditableTextBlock : UserControl {
#region Dependency Properties, Events
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(String), typeof(EditableTextBlock),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty IsEditableProperty =
DependencyProperty.Register("IsEditable", typeof(Boolean), typeof(EditableTextBlock), new PropertyMetadata(true));
public static readonly DependencyProperty IsInEditModeProperty =
DependencyProperty.Register("IsInEditMode", typeof(Boolean), typeof(EditableTextBlock), new PropertyMetadata(false));
public static readonly DependencyProperty TextFormatProperty =
DependencyProperty.Register("TextFormat", typeof(String), typeof(EditableTextBlock), new PropertyMetadata("{0}"));
#endregion ///Dependency Properties, Events
#region Variables and Properties
/// <summary>
/// We keep the old text when we go into editmode
/// in case the user aborts with the escape key
/// </summary>
private String oldText;
/// <summary>
/// Text content of this EditableTextBlock
/// </summary>
public String Text {
get { return (String)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
/// <summary>
/// Is this EditableTextBlock editable or not
/// </summary>
public Boolean IsEditable {
get { return (Boolean)GetValue(IsEditableProperty); }
set { SetValue(IsEditableProperty, value); }
}
/// <summary>
/// Is this EditableTextBlock currently in edit mode
/// </summary>
public Boolean IsInEditMode {
get {
if (IsEditable)
return (Boolean)GetValue(IsInEditModeProperty);
else
return false;
}
set {
if (IsEditable) {
if (value)
oldText = Text;
SetValue(IsInEditModeProperty, value);
}
}
}
/// <summary>
/// The text format for the TextBlock
/// </summary>
public String TextFormat {
get { return (String)GetValue(TextFormatProperty); }
set {
if (value == "")
value = "{0}";
SetValue(TextFormatProperty, value);
}
}
/// <summary>
/// The formatted text of this EditablTextBlock
/// </summary>
public String FormattedText {
get { return String.Format(TextFormat, Text); }
}
#endregion ///Variables and Properties
#region Constructor
/// <summary>
/// Default constructor for the editable text block
/// </summary>
public EditableTextBlock() {
InitializeComponent();
Focusable = true;
FocusVisualStyle = null;
}
#endregion ///Constructor
#region Methods, Functions and Eventhandler
/// <summary>
/// Invoked when we enter edit mode
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event arguments</param>
void TextBox_Loaded(object sender, RoutedEventArgs e) {
TextBox txt = sender as TextBox;
/// Give the TextBox input focus
txt.Focus();
txt.SelectAll();
}
/// <summary>
/// Invoked when we exit edit mode
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event arguments</param>
void TextBox_LostFocus(object sender, RoutedEventArgs e) {
IsInEditMode = false;
}
/// <summary>
/// Invoked when the user edits the annotation.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event arguments</param>
void TextBox_KeyDown(object sender, KeyEventArgs e) {
if (e.Key == Key.Enter) {
IsInEditMode = false;
e.Handled = true;
}
else if (e.Key == Key.Escape) {
IsInEditMode = false;
Text = oldText;
e.Handled = true;
}
}
/// <summary>
/// Invoked when the user double-clicks on the textblock
/// to edit the text
/// </summary>
/// <param name="sender">Sender (the Textblock)</param>
/// <param name="e">Event arguments</param>
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e) {
if (e.ClickCount == 2)
IsInEditMode = true;
}
#endregion ///Methods, Functions and Eventhandler
}
Thank you for any help!
Update 2
I changed the following line of code:
<TextBlock Text="{Binding ElementName=mainControl, Path=FormattedText}" Margin="5,3,5,3" MouseDown="TextBlock_MouseDown" />
to:
<TextBlock Text="{Binding ElementName=mainControl, Path=Text}" Margin="5,3,5,3" MouseDown="TextBlock_MouseDown" />
and now it is working!
I didn't see the TextBlock using the FormattedText in the first place! Ugh, thank you very much, now everything updates perfectly!
As postes by Lee O. the problem was indeed the bound property from my EditableTextBlock control.
The TextBlock used the FormattedText property that was updated by my Binding. Now I use the Text property for both, the TextBlock and the TextBox controls.
I simply removed the FormattedText property as well as the TextFormatProperty (DependencyProperty) and TextFormat property from my EditableTextBlock because I didn't plan to use those.
Thank you again!
I've created a custom user control. Is it possible for me to add a click event so that when someone clicks anywhere in the area of the control, a click event is fired?
The user control is defined as:
XAML:
<Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
<StackPanel Orientation="Vertical">
<Image Source="{Binding TabItemImage}" HorizontalAlignment="Center" Stretch="None" VerticalAlignment="Top" />
<TextBlock Text="{Binding TabItemText}" FontSize="15" HorizontalAlignment="Center" VerticalAlignment="Bottom" />
</StackPanel>
</Grid>
C#:
public partial class TabItem : UserControl
{
public static readonly DependencyProperty ImageProperty = DependencyProperty.Register("TabItemImage", typeof(string), typeof(TabItem), null);
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("TabItemText", typeof(string), typeof(TabItem), null);
public string TabItemImage
{
get { return (string)GetValue(ImageProperty); }
set { SetValue(ImageProperty, value); }
}
public string TabItemText
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public TabItem()
{
InitializeComponent();
this.DataContext = this;
}
}
With the usage simply:
<tabs:TabItem TabItemText="OVERVIEW" TabItemImage="/Resources/Images/overview.png" />
Ideally I'd be able to modify the user control so that I could specify the click event, e.g.
<tabs:TabItem
TabItemText="OVERVIEW"
TabItemImage="/Resources/Images/options_64.png"
Click="TabItem_Clicked"/> <!-- when someone clicks the control, this fires -->
Is this possible? If so, what do I need to do to create a click event on a custom user control?
You need to add custom RoutedEvent to your TabItem UserControl, Below is code to add a Custom RoutedEvent:
public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent(
"Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TabItem));
public event RoutedEventHandler Click
{
add { AddHandler(ClickEvent, value); }
remove { RemoveHandler(ClickEvent, value); }
}
void RaiseClickEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(TabItem.ClickEvent);
RaiseEvent(newEventArgs);
}
void OnClick()
{
RaiseClickEvent();
}
And then in your UserControl InitializeMethod wire up PreviewMouseLeftButtonUp event to fire your Custom RoutedEvent:
PreviewMouseLeftButtonUp += (sender, args) => OnClick();
There is a pretty good How-to on MSDN discussing this, you might want to read that.
This answer by Suresh has a good point and that would be a great way to do it. However, If you don't have more than one click event for this UserControl, you can you just use the any of the number of mouseclick events that come with defining a custom UserControl.
I didn't know you could set the datacontext to its self... That is interesting.
XAML:
<UserControl x:Class="StackTest.TestControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
MouseLeftButtonUp="TestControl_OnMouseLeftButtonUp"
MouseDoubleClick="TestControl_OnMouseDoubleClick"
MouseLeftButtonDown="TestControl_OnMouseLeftButtonDown">
<Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
<StackPanel Orientation="Vertical">
<Image Source="{Binding TabItemImage}" HorizontalAlignment="Center" Stretch="None" VerticalAlignment="Top" />
<TextBlock Text="{Binding TabItemText}" FontSize="15" HorizontalAlignment="Center" VerticalAlignment="Bottom" />
</StackPanel>
</Grid>
</UserControl>
CS:
public partial class TestControl : UserControl
{
public static readonly DependencyProperty ImageProperty = DependencyProperty.Register("TabItemImage" , typeof(string) , typeof(TabItem) , null);
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("TabItemText" , typeof(string) , typeof(TabItem) , null);
public string TabItemImage
{
get { return (string)GetValue(ImageProperty); }
set { SetValue(ImageProperty , value); }
}
public string TabItemText
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty , value); }
}
public TestControl()
{
InitializeComponent();
this.DataContext = this;
}
// or
private void TestControl_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
// Add logic...
}
// or
private void TestControl_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Add logic...
}
// or
private void TestControl_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// Add logic...
}
}
This answer by Suresh requires the following namespace declarations:
using System;
using System.Windows;
using System.Windows.Controls;
namespace YourNameSpace
{
partial class OPButton
{
/// <summary>
/// Create a custom routed event by first registering a RoutedEventID
/// This event uses the bubbling routing strategy
/// see the web page https://msdn.microsoft.com/EN-US/library/vstudio/ms598898(v=vs.90).aspx
/// </summary>
public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(OPButton));
/// <summary>
/// Provide CLR accessors for the event Click OPButton
/// Adds a routed event handler for a specified routed event Click, adding the handler to the handler collection on the current element.
/// </summary>
public event RoutedEventHandler Click
{
add {AddHandler(ClickEvent, value); }
remove { RemoveHandler(ClickEvent, value); }
}
/// <summary>
/// This method raises the Click event
/// </summary>
private void RaiseClickEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(OPButton.ClickEvent);
RaiseEvent(newEventArgs);
}
/// <summary>
/// For isPressed purposes we raise the event when the OPButton is clicked
/// </summary>
private void OnClick()
{
RaiseClickEvent();
}
}
}
And the following References:
Windows;
PresentationCore;
WindowsBase;