I have a TextBlock that's on top of a Button in a Grid. I'd like to have then displayed thus:
"some very long text" <-- text
"that doesn't fit" <-- text wrapped
[my button text size] <-- button
However, what I've got is this:
"some very long text that doesn't fit" <-- text
[my button text size] <-- button
My issue is that the text in the Button is dynamically set through localized resource and therefore the width of the button changes dynamically.
The static solution that works for non-dynamic Button resize is:
<TextBlock
Margin="5"
TextWrapping="Wrap"
Width="{Binding ElementName=requestDemoButton, Path=RenderSize.Width}"
Text="{Binding Path=Resource.Text, Source={StaticResource LocalizedStrings }}"
/>
<Button
x:Name="requestDemoButton"
Margin="5"
Height="Auto"
Width="Auto"
HorizontalAlignment="Right"
Content="{Binding Path=Resource.Button, Source={StaticResource LocalizedStrings }}" />
Ideas, anyone? I'm currently thinking of sticking a Behavior class to the TextBlock that listens for the SizeChanged event on the Button. I'd like to have a built-in solution if it exists.
If anyone's interested, here's how I've done in in a behaviour.
I pass on Height or Width according to the property I want bound.
Here's the class:
public static class DynamicControlResizeBehavior
{
public static readonly DependencyProperty TargetProperty =
DependencyProperty.RegisterAttached("Target", typeof(FrameworkElement), typeof(DynamicControlResizeBehavior), new PropertyMetadata(OnTargetSetCallback));
public static readonly DependencyProperty PropertyNameProperty =
DependencyProperty.RegisterAttached("PropertyName", typeof(string), typeof(DynamicControlResizeBehavior), new PropertyMetadata(OnPropertyNameSetCallback));
public static string GetPropertyName(DependencyObject obj)
{
return (string)obj.GetValue(PropertyNameProperty);
}
public static void SetPropertyName(DependencyObject obj, string value)
{
obj.SetValue(PropertyNameProperty, value);
}
public static FrameworkElement GetTarget(DependencyObject obj)
{
return (FrameworkElement)obj.GetValue(TargetProperty);
}
public static void SetTarget(DependencyObject obj, FrameworkElement value)
{
obj.SetValue(TargetProperty, value);
}
private static void SynchronizeProperty(DependencyObject dependencyObject)
{
var target = GetTarget(dependencyObject);
if (target != null)
{
var propertyName = GetPropertyName(dependencyObject);
DependencyProperty dependencyToRead;
DependencyProperty dependencyToWrite;
if (string.Equals(propertyName, "Width", StringComparison.InvariantCulture))
{
dependencyToRead = FrameworkElement.ActualWidthProperty;
dependencyToWrite = FrameworkElement.WidthProperty;
}
else if (string.Equals(propertyName, "Height", StringComparison.InvariantCulture))
{
dependencyToRead = FrameworkElement.ActualHeightProperty;
dependencyToWrite = FrameworkElement.HeightProperty;
}
else
{
return;
}
var propertySize = (double)target.GetValue(dependencyToRead);
dependencyObject.SetValue(dependencyToWrite, propertySize);
}
}
private static void OnTargetSetCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var oldElement = e.OldValue as FrameworkElement;
if (oldElement != null)
{
oldElement.SizeChanged -= (o, s) => SynchronizeProperty(d);
}
var newElement = e.NewValue as FrameworkElement;
if (newElement != null)
{
newElement.SizeChanged += (o, s) => SynchronizeProperty(d);
}
}
private static void OnPropertyNameSetCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SynchronizeProperty(d);
}
}
And here's how it's used:
<TextBlock
Behaviors:DynamicControlResizeBehavior.Target="{Binding ElementName=submitButton}"
Behaviors:DynamicControlResizeBehavior.PropertyName="Width"
HorizontalAlignment="Right"
Margin="5,20,5,5"
TextWrapping="Wrap"
Text="{Binding Path=Resource.RequestDemoLoginText, Source={StaticResource LocalizedStrings }}"
/>
<Button
x:Name="submitButton"
Margin="5"
Height="Auto"
Width="Auto"
HorizontalAlignment="Right"
HorizontalContentAlignment="Left"
Content="{Binding Path=Resource.RequestDemoLogin, Source={StaticResource LocalizedStrings }}" />
Hope that might help someone else.
Related
I have a button and a popup control in it in my xaml as follows:
<Button Grid.Column="0" Grid.Row="5"
HorizontalAlignment="Center"
Margin="88,8,214,0"
Grid.RowSpan="2" Height="26"
VerticalAlignment="Top" Width="22"
IsEnabled="{Binding Path=SearchFound}"
x:Name="cmdPlanList"
Click="cmdPlanList_Click">
<ContentControl>
<Popup IsOpen = "{Binding PlanPopup}"
PlacementTarget = "{Binding ElementName = cmdPlanList}"
AllowsTransparency = "True"
PopupAnimation = "Slide"
x:Name="Popup4Plan">
<Canvas Width = "125"
Height = "100"
Background = "Red" >
<Canvas.RenderTransform>
<RotateTransform x:Name = "theTransform" />
</Canvas.RenderTransform>
<TextBlock TextWrapping = "Wrap"
Foreground = "Blue"
Text = "Hi, this is Popup" />
</Canvas>
</Popup>
</ContentControl>
</Button>
I am setting the DataContext of this Popup from my code-behind as follows:-
My View's code behind:-
using xyz
{
private bool _PlanPopup = false;
public bool PlanPopup
{
get { return _PlanPopup; }
set
{
_PlanPopup = value;
}
}
public MyView()
{
InitializeComponent();
Popup4Plan.DataContext = this;
}
private void cmdPlanList_Click(object sender, RoutedEventArgs e)
{
this.PlanPopup = this.PlanPopup ? false : true;
}
}
If you want to bind the View to a property of itself, make the property a dependency property.
public bool IsOpen
{
get
{
return (bool)GetValue(IsOpenProperty);
}
set
{
SetValue(IsOpenProperty, value);
}
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register("IsOpen", typeof(bool), typeof(MyView), new PropertyMetadata(false));
To quickly make it type propdp [tab][tab] and fill in the blanks.
Also:
this.PlanPopup = this.PlanPopup ? false : true;
looks much better this way:
this.PlanPopup = !this.PlanPopup;
For updates on bound properties to work you need to implement INotifyPropertyChanged. For example like the following.
public class XYZ : INotifyPropertyChanged
{
private bool isOpen;
public bool IsOpen {
get { return this.isOpen; }
set {
this.isOpen = value;
this.OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
With this code an instance of XYZ will notify that the property IsOpen has changed and any bound view elements will re-fetch the value of IsOpen.
My TextBox has a limit of 100 characters set by MaxLength property.
However, if the user types '\n' or '\t', they are counted as an additional character, which would make sense to the programmer, but not to the user.
Is there any workaround besides counting the characters by myself?
You could create your own attached property:
<TextBox wpfApplication4:TextBoxBehaviors.MaxLengthIgnoringWhitespace="10" />
With the attached property defined like this:
public static class TextBoxBehaviors
{
public static readonly DependencyProperty MaxLengthIgnoringWhitespaceProperty = DependencyProperty.RegisterAttached(
"MaxLengthIgnoringWhitespace",
typeof(int),
typeof(TextBoxBehaviors),
new PropertyMetadata(default(int), MaxLengthIgnoringWhitespaceChanged));
private static void MaxLengthIgnoringWhitespaceChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
var textBox = dependencyObject as TextBox;
if (textBox != null && eventArgs.NewValue is int)
{
textBox.TextChanged += (sender, args) =>
{
var maxLength = ((int)eventArgs.NewValue) + textBox.Text.Count(char.IsWhiteSpace);
textBox.MaxLength = maxLength;
};
}
}
public static void SetMaxLengthIgnoringWhitespace(DependencyObject element, int value)
{
element.SetValue(MaxLengthIgnoringWhitespaceProperty, value);
}
public static int GetMaxLengthIgnoringWhitespace(DependencyObject element)
{
return (int)element.GetValue(MaxLengthIgnoringWhitespaceProperty);
}
}
The code will use the TextBox's existing MaxLength property and will just increase it by the number of white spaces you have entered. So if you set the property to 10 and type in 5 spaces, the actual MaxLength on the TextBox will be set to 15, and so on.
I really enjoy Toby Crawfordanswer but since i started to try a simple answer i like to add mine :
public partial class MainWindow : Window
{
public string TextLength { get; set; }
public MainWindow()
{
InitializeComponent();
}
private void txtInput_TextChanged(object sender, TextChangedEventArgs e)
{
var textbox = sender as TextBox;
var tempText = textbox.Text.Replace(" ", "");
lblLength.Content = (tempText.Length).ToString();
}
}
<Grid>
<TextBox HorizontalAlignment="Left" Name="txtInput" MaxLength="{Binding TextMaxLength}" Height="23" Margin="220,67,0,0" TextWrapping="Wrap" Text="" TextChanged="txtInput_TextChanged" VerticalAlignment="Top" Width="120"/>
<Label Name="lblLength" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="220,126,0,0"/>
<Label Content="Your text length" HorizontalAlignment="Left" Margin="93,126,0,0" VerticalAlignment="Top"/>
<Label Content="Your text" HorizontalAlignment="Left" Margin="93,67,0,0" VerticalAlignment="Top"/>
</Grid>
I'm using MVVM.
<ItemsControl ItemsSource="{Binding AllIcons}" Tag="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<Label HorizontalAlignment="Right">x</Label>
<Image Source="{Binding Source}" Height="100" Width="100" />
<Label HorizontalAlignment="Center" Content="{Binding Title}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
That looks fine. If I put a button in the stack panel using this command:
<Button Command="{Binding Path=DataContext.InvasionCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}" CommandParameter="{Binding}"/>
I'm able to capture the command. However, I want to execute the command binding when the mouse enters the stack panel, not when I click a button.
Any idea?
My wrong, input bindings does not solve the problem. You may use attached properties for this:
public static class MouseEnterCommandBinding
{
public static readonly DependencyProperty MouseEnterCommandProperty = DependencyProperty.RegisterAttached(
"MouseEnterCommand",
typeof(ICommand),
typeof(MouseEnterCommandBinding),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)
);
public static void SetMouseEnterCommand(UIElement element, ICommand value)
{
element.SetValue(MouseEnterCommandProperty, value);
element.MouseEnter += (s,e) =>
{
var uiElement = s as UIElement;
var command = GetMouseEnterCommand(uiElement);
if (command != null && command.CanExecute(uiElement.CommandParameter))
command.Execute(uiElement.CommandParameter);
}
}
public static ICommand GetMouseEnterCommand(UIElement element)
{
return element.GetValue(MouseEnterCommandProperty) as ICommand;
}
}
First you need to declare a behavior for mouse enter. This basically translates the event into a command in your ViewModel.
public static class MouseEnterBehavior
{
public static readonly DependencyProperty MouseEnterProperty =
DependencyProperty.RegisterAttached("MouseEnter",
typeof(ICommand),
typeof(MouseEnterBehavior),
new PropertyMetadata(null, MouseEnterChanged));
public static ICommand GetMouseEnter(DependencyObject obj)
{
return (ICommand)obj.GetValue(MouseEnterProperty);
}
public static void SetMouseEnter(DependencyObject obj, ICommand value)
{
obj.SetValue(MouseEnterProperty, value);
}
private static void MouseEnterChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
UIElement uiElement = obj as UIElement;
if (uiElement != null)
uiElement.MouseEnter += new MouseEventHandler(uiElement_MouseEnter);
}
static void uiElement_MouseEnter(object sender, MouseEventArgs e)
{
UIElement uiElement = sender as UIElement;
if (uiElement != null)
{
ICommand command = GetMouseEnter(uiElement);
command.Execute(uiElement);
}
}
}
Then you just need to create that command in your view model and reference it in the view. The behaviors: namespace should just point to wherever you created that behavior. I use this pattern every time I need to translate an event into a command in a view model.
<Grid>
<StackPanel behaviors:MouseEnterBehavior.MouseEnter="{Binding MouseEnteredCommand}"
Height="150"
Width="150"
Background="Red">
</StackPanel>
</Grid>
You probably need to use InputBindings: http://msdn.microsoft.com/en-us/library/system.windows.input.inputbinding.aspx
I need to create a Dialog / Prompt including TextBox for user input. My problem is, how to get the text after having confirmed the dialog? Usually I would make a class for this which would save the text in a property. However I want do design the Dialog using XAML. So I would somehow have to extent the XAML Code to save the content of the TextBox in a property - but I guess that's not possible with pure XAML. What would be the best way to realize what I'd like to do? How to build a dialog which can be defined from XAML but can still somehow return the input? Thanks for any hint!
The "responsible" answer would be for me to suggest building a ViewModel for the dialog and use two-way databinding on the TextBox so that the ViewModel had some "ResponseText" property or what not. This is easy enough to do but probably overkill.
The pragmatic answer would be to just give your text box an x:Name so that it becomes a member and expose the text as a property in your code behind class like so:
<!-- Incredibly simplified XAML -->
<Window x:Class="MyDialog">
<StackPanel>
<TextBlock Text="Enter some text" />
<TextBox x:Name="ResponseTextBox" />
<Button Content="OK" Click="OKButton_Click" />
</StackPanel>
</Window>
Then in your code behind...
partial class MyDialog : Window {
public MyDialog() {
InitializeComponent();
}
public string ResponseText {
get { return ResponseTextBox.Text; }
set { ResponseTextBox.Text = value; }
}
private void OKButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
DialogResult = true;
}
}
Then to use it...
var dialog = new MyDialog();
if (dialog.ShowDialog() == true) {
MessageBox.Show("You said: " + dialog.ResponseText);
}
Edit: Can be installed with nuget https://www.nuget.org/packages/PromptDialog/
I just add a static method to call it like a MessageBox:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
x:Class="utils.PromptDialog"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStartupLocation="CenterScreen"
SizeToContent="WidthAndHeight"
MinWidth="300"
MinHeight="100"
WindowStyle="SingleBorderWindow"
ResizeMode="CanMinimize">
<StackPanel Margin="5">
<TextBlock Name="txtQuestion" Margin="5"/>
<TextBox Name="txtResponse" Margin="5"/>
<PasswordBox Name="txtPasswordResponse" />
<StackPanel Orientation="Horizontal" Margin="5" HorizontalAlignment="Right">
<Button Content="_Ok" IsDefault="True" Margin="5" Name="btnOk" Click="btnOk_Click" />
<Button Content="_Cancel" IsCancel="True" Margin="5" Name="btnCancel" Click="btnCancel_Click" />
</StackPanel>
</StackPanel>
</Window>
And the code behind:
public partial class PromptDialog : Window
{
public enum InputType
{
Text,
Password
}
private InputType _inputType = InputType.Text;
public PromptDialog(string question, string title, string defaultValue = "", InputType inputType = InputType.Text)
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(PromptDialog_Loaded);
txtQuestion.Text = question;
Title = title;
txtResponse.Text = defaultValue;
_inputType = inputType;
if (_inputType == InputType.Password)
txtResponse.Visibility = Visibility.Collapsed;
else
txtPasswordResponse.Visibility = Visibility.Collapsed;
}
void PromptDialog_Loaded(object sender, RoutedEventArgs e)
{
if (_inputType == InputType.Password)
txtPasswordResponse.Focus();
else
txtResponse.Focus();
}
public static string Prompt(string question, string title, string defaultValue = "", InputType inputType = InputType.Text)
{
PromptDialog inst = new PromptDialog(question, title, defaultValue, inputType);
inst.ShowDialog();
if (inst.DialogResult == true)
return inst.ResponseText;
return null;
}
public string ResponseText
{
get
{
if (_inputType == InputType.Password)
return txtPasswordResponse.Password;
else
return txtResponse.Text;
}
}
private void btnOk_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
Close();
}
}
So you can call it like:
string repeatPassword = PromptDialog.Prompt("Repeat password", "Password confirm", inputType: PromptDialog.InputType.Password);
Great answer of Josh, all credit to him, I slightly modified it to this however:
MyDialog Xaml
<StackPanel Margin="5,5,5,5">
<TextBlock Name="TitleTextBox" Margin="0,0,0,10" />
<TextBox Name="InputTextBox" Padding="3,3,3,3" />
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Name="BtnOk" Content="OK" Grid.Column="0" Margin="0,0,5,0" Padding="8" Click="BtnOk_Click" />
<Button Name="BtnCancel" Content="Cancel" Grid.Column="1" Margin="5,0,0,0" Padding="8" Click="BtnCancel_Click" />
</Grid>
</StackPanel>
MyDialog Code Behind
public MyDialog()
{
InitializeComponent();
}
public MyDialog(string title,string input)
{
InitializeComponent();
TitleText = title;
InputText = input;
}
public string TitleText
{
get { return TitleTextBox.Text; }
set { TitleTextBox.Text = value; }
}
public string InputText
{
get { return InputTextBox.Text; }
set { InputTextBox.Text = value; }
}
public bool Canceled { get; set; }
private void BtnCancel_Click(object sender, System.Windows.RoutedEventArgs e)
{
Canceled = true;
Close();
}
private void BtnOk_Click(object sender, System.Windows.RoutedEventArgs e)
{
Canceled = false;
Close();
}
And call it somewhere else
var dialog = new MyDialog("test", "hello");
dialog.Show();
dialog.Closing += (sender,e) =>
{
var d = sender as MyDialog;
if(!d.Canceled)
MessageBox.Show(d.InputText);
}
You don't need ANY of these other fancy answers. Below is a simplistic example that doesn't have all the Margin, Height, Width properties set in the XAML, but should be enough to show how to get this done at a basic level.
XAML
Build a Window page like you would normally and add your fields to it, say a Label and TextBox control inside a StackPanel:
<StackPanel Orientation="Horizontal">
<Label Name="lblUser" Content="User Name:" />
<TextBox Name="txtUser" />
</StackPanel>
Then create a standard Button for Submission ("OK" or "Submit") and a "Cancel" button if you like:
<StackPanel Orientation="Horizontal">
<Button Name="btnSubmit" Click="btnSubmit_Click" Content="Submit" />
<Button Name="btnCancel" Click="btnCancel_Click" Content="Cancel" />
</StackPanel>
Code-Behind
You'll add the Click event handler functions in the code-behind, but when you go there, first, declare a public variable where you will store your textbox value:
public static string strUserName = String.Empty;
Then, for the event handler functions (right-click the Click function on the button XAML, select "Go To Definition", it will create it for you), you need a check to see if your box is empty. You store it in your variable if it is not, and close your window:
private void btnSubmit_Click(object sender, RoutedEventArgs e)
{
if (!String.IsNullOrEmpty(txtUser.Text))
{
strUserName = txtUser.Text;
this.Close();
}
else
MessageBox.Show("Must provide a user name in the textbox.");
}
Calling It From Another Page
You're thinking, if I close my window with that this.Close() up there, my value is gone, right? NO!! I found this out from another site: http://www.dreamincode.net/forums/topic/359208-wpf-how-to-make-simple-popup-window-for-input/
They had a similar example to this (I cleaned it up a bit) of how to open your Window from another and retrieve the values:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void btnOpenPopup_Click(object sender, RoutedEventArgs e)
{
MyPopupWindow popup = new MyPopupWindow(); // this is the class of your other page
//ShowDialog means you can't focus the parent window, only the popup
popup.ShowDialog(); //execution will block here in this method until the popup closes
string result = popup.strUserName;
UserNameTextBlock.Text = result; // should show what was input on the other page
}
}
Cancel Button
You're thinking, well what about that Cancel button, though? So we just add another public variable back in our pop-up window code-behind:
public static bool cancelled = false;
And let's include our btnCancel_Click event handler, and make one change to btnSubmit_Click:
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
cancelled = true;
strUserName = String.Empty;
this.Close();
}
private void btnSubmit_Click(object sender, RoutedEventArgs e)
{
if (!String.IsNullOrEmpty(txtUser.Text))
{
strUserName = txtUser.Text;
cancelled = false; // <-- I add this in here, just in case
this.Close();
}
else
MessageBox.Show("Must provide a user name in the textbox.");
}
And then we just read that variable in our MainWindow btnOpenPopup_Click event:
private void btnOpenPopup_Click(object sender, RoutedEventArgs e)
{
MyPopupWindow popup = new MyPopupWindow(); // this is the class of your other page
//ShowDialog means you can't focus the parent window, only the popup
popup.ShowDialog(); //execution will block here in this method until the popup closes
// **Here we find out if we cancelled or not**
if (popup.cancelled == true)
return;
else
{
string result = popup.strUserName;
UserNameTextBlock.Text = result; // should show what was input on the other page
}
}
Long response, but I wanted to show how easy this is using public static variables. No DialogResult, no returning values, nothing. Just open the window, store your values with the button events in the pop-up window, then retrieve them afterwards in the main window function.
I have found this question MVVM and the TextBox's SelectedText property. However, I am having trouble getting the solution given to work. This is my non-working code, in which I am trying to display the first textbox's selected text in the second textbox.
View:
SelectedText and Text are just string properties from my ViewModel.
<TextBox Text="{Binding Path=Text, UpdateSourceTrigger=PropertyChanged}" Height="155" HorizontalAlignment="Left" Margin="68,31,0,0" Name="textBox1" VerticalAlignment="Top" Width="264" AcceptsReturn="True" AcceptsTab="True" local:TextBoxHelper.SelectedText="{Binding SelectedText, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}" />
<TextBox Text="{Binding SelectedText, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Height="154" HorizontalAlignment="Left" Margin="82,287,0,0" Name="textBox2" VerticalAlignment="Top" Width="239" />
TextBoxHelper
public static class TextBoxHelper
{
#region "Selected Text"
public static string GetSelectedText(DependencyObject obj)
{
return (string)obj.GetValue(SelectedTextProperty);
}
public static void SetSelectedText(DependencyObject obj, string value)
{
obj.SetValue(SelectedTextProperty, value);
}
// Using a DependencyProperty as the backing store for SelectedText. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedTextProperty =
DependencyProperty.RegisterAttached(
"SelectedText",
typeof(string),
typeof(TextBoxHelper),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedTextChanged));
private static void SelectedTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
TextBox tb = obj as TextBox;
if (tb != null)
{
if (e.OldValue == null && e.NewValue != null)
{
tb.SelectionChanged += tb_SelectionChanged;
}
else if (e.OldValue != null && e.NewValue == null)
{
tb.SelectionChanged -= tb_SelectionChanged;
}
string newValue = e.NewValue as string;
if (newValue != null && newValue != tb.SelectedText)
{
tb.SelectedText = newValue as string;
}
}
}
static void tb_SelectionChanged(object sender, RoutedEventArgs e)
{
TextBox tb = sender as TextBox;
if (tb != null)
{
SetSelectedText(tb, tb.SelectedText);
}
}
#endregion
}
What am I doing wrong?
The reason this is not working is that the property change callback isn't being raised (as the bound value from your VM is the same as the default value specified in the metadata for the property). More fundamentally though, your behavior will detach when the selected text is set to null. In cases like this, I tend to have another attached property that is simply used to enable the monitoring of the selected text, and then the SelectedText property can be bound. So, something like so:
#region IsSelectionMonitored
public static readonly DependencyProperty IsSelectionMonitoredProperty = DependencyProperty.RegisterAttached(
"IsSelectionMonitored",
typeof(bool),
typeof(PinnedInstrumentsViewModel),
new FrameworkPropertyMetadata(OnIsSelectionMonitoredChanged));
[AttachedPropertyBrowsableForType(typeof(TextBox))]
public static bool GetIsSelectionMonitored(TextBox d)
{
return (bool)d.GetValue(IsSelectionMonitoredProperty);
}
public static void SetIsSelectionMonitored(TextBox d, bool value)
{
d.SetValue(IsSelectionMonitoredProperty, value);
}
private static void OnIsSelectionMonitoredChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
TextBox tb = obj as TextBox;
if (tb != null)
{
if ((bool)e.NewValue)
{
tb.SelectionChanged += tb_SelectionChanged;
}
else
{
tb.SelectionChanged -= tb_SelectionChanged;
}
SetSelectedText(tb, tb.SelectedText);
}
}
#endregion
#region "Selected Text"
public static string GetSelectedText(DependencyObject obj)
{
return (string)obj.GetValue(SelectedTextProperty);
}
public static void SetSelectedText(DependencyObject obj, string value)
{
obj.SetValue(SelectedTextProperty, value);
}
// Using a DependencyProperty as the backing store for SelectedText. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedTextProperty =
DependencyProperty.RegisterAttached(
"SelectedText",
typeof(string),
typeof(TextBoxHelper),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedTextChanged));
private static void SelectedTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
TextBox tb = obj as TextBox;
if (tb != null)
{
tb.SelectedText = e.NewValue as string;
}
}
static void tb_SelectionChanged(object sender, RoutedEventArgs e)
{
TextBox tb = sender as TextBox;
if (tb != null)
{
SetSelectedText(tb, tb.SelectedText);
}
}
#endregion
And then in your XAML, you'd have to add that property to your first TextBox:
<TextBox ... local:TextBoxHelper.IsSelectionMonitored="True" local:TextBoxHelper.SelectedText="{Binding SelectedText, Mode=OneWayToSource}" />
In order for the SelectedTextChanged handler to fire the SelectedText property must have an initial value. If you don't initialize this to some value (string.Empty as a bare minimum) then this handler will never fire and in turn you'll never register the tb_SelectionChanged handler.
This works for me using the class TextBoxHelper. As other mentioned, you need to initialize the SelectedText property of TextBoxHelper with a non null value. Instead of data binding to a string property (SelText) on the view you should bind to a string property of your VM which should implement INotifyPropertyChanged.
XAML:
<Window x:Class="TextSelectDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TextSelectDemo"
Height="300" Width="300">
<StackPanel>
<TextBox local:TextBoxHelper.SelectedText="{Binding Path=SelText, Mode=TwoWay}" />
<TextBox Text="{Binding Path=SelText}" />
</StackPanel>
</Window>
Code behind:
using System.ComponentModel;
using System.Windows;
namespace TextSelectDemo
{
public partial class Window1 : Window, INotifyPropertyChanged
{
public Window1()
{
InitializeComponent();
SelText = string.Empty;
DataContext = this;
}
private string _selText;
public string SelText
{
get { return _selText; }
set
{
_selText = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("SelText"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Your binding attempts to bind the Text property of your TextBox to a SelectedText property of the TextBox's current data context. Since you're working with an attached property, not with a property hanging off of your data context, you will need to give more information in your binding:
<TextBox Text="{Binding local:TextBoxHelper.SelectedText, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" ... />
Where local has been associated with the CLR namespace containing the TextBoxHelper class.
You need a normal .net property wrapper for the dependencyproperty, some like:
public string SelectedText
{
set {SetSelectedText(this, value);}
...
It is not required by runtime (runtime use set/get) but it is required by designer and compiler.