Binding to TextBlock Width property in MVVM - c#

I have a TextBlock control in my view, its Width depends on the Text property.
I'm looking for some way to bind the TextBlocks Width to a property in my model,which will work as follows:
The setting of the Width must be done automatically based on Text
In my button click I would like to retrieve the Width
I've tried the code below, but it keeps the Width as 0 if I don't explicitly set it in the constructor of the view model.Tried Mode=OneWayToSource and Mode=OneWay but it made no difference, any suggestions?
View:
<Grid>
<TextBlock Text="Some text" Width="{Binding TextWidth,Mode=OneWayToSource}" />
<Button Content="Show Width" Height="30" Width="90" Command="{Binding ShowTextWidth}" />
</Grid>
View model:
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private DelegateCommand<object> showTextWidth;
public DelegateCommand<object> ShowTextWidth
{
get { return showTextWidth; }
set { showTextWidth = value; }
}
private double textWidth;
public double TextWidth
{
get { return textWidth; }
set
{
textWidth = value;
OnPropertyChanged("TextWidth");
}
}
public ViewModel()
{
//If I explicitly specify the width it works:
//TextWidth = 100;
ShowTextWidth = new DelegateCommand<object>(ShowWidth);
}
private void ShowWidth(object parameter)
{
MessageBox.Show(TextWidth.ToString());
}
}

Ended up creating an attached behavior by Maleak which was inspired by Kent Boogaarts Pushing read-only GUI properties back into ViewModel, can't believe it's so complicated to push the value of ActualWidth into the view model!

Width is a DependencyProperty on TextBlock. In this case it's a Target for Binding and TextWidth is your source for Binding. OneWayToSource seems like the way to go, you are setting TextWidth to 100 in the ViewModel which does not propogate to Width on TextBlock because it's OneWayToSource yes correct, Width (Target) is then setting TextWidth (Source) to Double.NaN because of OneWayToSource and that's why you're seeing 0...
ActualWidth should work like sa_ddam213 said but also consider that your TextBlock doesn't grow in Width (ActualWidth) when the Text changes because it is spanning the total width of your Grid layout. Either put it in a ColumnDefinition with Width set to Auto or make your HorizontalAlignment Left to see the ActualWidth change when the Text changes.
I have made some changes to your source code. Consider using CommandParameter on your button? Check out the link...
WpfApplication10.zip

you dont really need to set the Width if you choose a layout panel wich can handle this for you.
eg. a columndefinition with width=auto grows with your text

To set the Width depending on its containing Text you could bind the Width-Property to the Text-Property and use a converter like so:
<TextBlock Width="{Binding RelativeSource={RelativeSource Self}, Path=Text, Converter={StaticResource TextToWidth}}"
Your converter could look like:
TextBlock tb = new TextBlock();
tb.Text = value.ToString();
tb.Measure(new Size(int.MaxValue, 20)); //20 if there shouldnt be a linebreak
return tb.DesiredSize.Width;

Related

XAML label to C# code for creating dynamic labels [duplicate]

This question already has answers here:
How can I dynamically create a list of label and textboxes in WPF?
(2 answers)
How to generate labels, buttons, Checkboxes and textboxes dynamically in WPF App
(1 answer)
create dynamically label at wpf
(1 answer)
Closed 3 years ago.
I created labels in XAML that are multiline and all lines are centered, tested and working perfectly fine. Now I want to create these labels dynamically but unfortunately I don't know how to do this.
tried setting LineStackingStrategy on MaxHeight, Fontfamily
tried creating new Textblock on fontfamily
XAML Code to reproduce in C# code:
<Label HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Name="btn_02" Height="70" Width="160" BorderBrush="#F0F2A4" BorderThickness="2" Background="Transparent" Foreground="#F0F2A4" FontFamily="Arial" FontSize="13" TextBlock.LineStackingStrategy="BlockLineHeight" TextBlock.LineHeight="17" Content="ORGEL
LADEN" TextBlock.TextAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,200,0,0" MouseLeftButtonDown="Btn_02_MouseLeftButtonDown"/>
C# code to create label dynamically:
Label label = new Label()
{
HorizontalAlignment = HorizontalAlignment.Center,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top,
VerticalContentAlignment = VerticalAlignment.Center,
Name = "orgel" + i.ToString(),
Height = 70,
Width = 160,
BorderBrush = (SolidColorBrush)(new BrushConverter().ConvertFrom("#F0F2A4")),
BorderThickness = new Thickness(2),
Background = Brushes.Transparent,
Foreground = (SolidColorBrush)(new BrushConverter().ConvertFrom("#F0F2A4")),
FontFamily = new FontFamily("Arial"),
FontSize = 13,
Content = orgel,
Margin = new Thickness((columncount * 100), (rowcount * 100), 0, 0)
};
I want to add TextBlock.LineStackingStrategy="BlockLineHeight" TextBlock.LineHeight="17" Content="ORGEL
LADEN" TextBlock.TextAlignment="Center" from XAML to this C# code.
Whenever you want to create UI elements dynamically you should think of a DataTemplate first. Use a ItemsControl to create UI elements simply by adding data to the ObservableCollection that serves as the Binding.Source of ItemsControl.ItemsSource. The DataTemplate is used to define the appearance of the items (a Labelin your case).
Data Templating Overview
ViewModel.cs
class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<string> labelContents;
public ObservableCollection<string> LabelContents
{
get => this.labelContents;
set
{
this.labelContents = value;
OnPropertyChanged();
}
}
// Constructor
public void ViewModel()
{
// Initialize the data binding source of the ListView
this.LabelContents = new ObservableCollection<string>();
}
public void CreateLabelDynamically(string labelText)
{
this.LabelContents.Add(labelText);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
When executing CreateLabelDynamically() a new label in form of a TextBlock is added to the view.
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<ListView x:Name="SpectrumBars"
ItemsSource="{Binding LabelContents}">
<ListView.ItemTemplate>
<DataTemplate DataType="system:string">
<!-- The DataContext of the TextBlock is the string value contained in LabelContents -->
<TextBlock Text="{Binding}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Window>
It is also recommended to use a TextBlock instead of a Label when possible:
When data binding the Label element's Content property to the String source object, you may experience poor performance.
Avoid Databinding to the Label.Content Property

Re-render a usercontrol after removing it from a container

I have UserControl which I display like this, it's for previewing a receipt ticket before printing:
<ScrollViewer x:Name="xzScroll" Template="{StaticResource scrollView}" Height="488" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch">
<Border x:Name="xzPreview" BorderThickness="0" Background="White" Margin="0,0,10,0" Padding="15">
// The UserControl
</Border>
</ScrollViewer>
When the user wants to print the receipt, I remove the UserControl from its parent Border, and try to re-measure and re-arrange the control according to the printeable area:
public void Print()
{
PrintDialog dlg = new PrintDialog();
this.Measure(new Size(dlg.PrintableAreaWidth, double.PositiveInfinity));
this.Arrange(new Rect(this.DesiredSize));
this.UpdateLayout();
dlg.PrintTicket.PageMediaSize = new PageMediaSize(this.ActualWidth, this.ActualHeight);
....
}
However, the Height and Width properties won't change, resulting in a faulty print with messed up margins and some text clipping on the sides.
When I create a new instance of my UserControl in code, and then use the Measure and Arrange methods, I see the Width and Height properties change according to the PrinteableAreaWidth, resulting in a proper receipt ticket.
How can I make sure the Width and Height properties are properly getting set?
Is it right that you want to to resize the Usercontrol?
If yes, why don't create a Class (ViewModel) for the Usercontrol with the two Properties Height and Width and bind the User Control to these Properties.
Than you can change the size without removing the Usercontrol.
Here for Example how such a ViewModel could look like. You will need to implement the INotifyPropertyChanged.
public class UserControlViewModel:INotifyPropertyChanged
{
private double _height;
private double _width;
public double _height
{
get { return _height;}
set {
if(_height == value)
return;
_height = value;
OnPropertyChanged();
}
public double _width
{
get { return _width;}
set {
if(_width == value)
return;
_width = value;
OnPropertyChanged();
}
And in Xaml you can bind it this way.
<ScrollViewer x:Name="xzScroll" Template="{StaticResource scrollView}" Height="488" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch">
<Border x:Name="xzPreview" BorderThickness="0" Background="White" Margin="0,0,10,0" Padding="15">
<MyUserControl
DataContext = "{Binding MyControlViewModelObject}"
Height = "{Binding Height}"
Width = "{Binding Width}"/>
</Border>
</ScrollViewer>
It is possible that you have declared the UI elements in your UserControl with exact Margin and/or Height and Width dimensions... of course, I can't be sure about that because you didn't show us your relevant code.
However, if this is the case, then you won't be able to rearrange them by re-sizing the UserControl externally. The solution is to use Grid controls to organise your UI elements, so that they can be automatically re-sized when the UserControl is resized. See the Grid Class page on MSDN for further information.

WPF DataGrid, Template Column and Virtualization

I have created VM classes for the DataGrid items (Rows and Cells).
The VM class for Cell is shown below:
public class ListGridCell : INotifyPropertyChanged
{
public ListGridCell(string Name)
{
// Init properties
this.Name = Name;
this.DataValue = null;
this.DataEditor = null;
}
public string Name { get; private set; }
private object _DataValue;
public object DataValue
{
get { return _DataValue; }
set { _DataValue = value; NotifyPropertyChanged("DataValue"); }
}
private FrameworkElement _DataEditor;
public FrameworkElement DataEditor
{
get { return _DataEditor; }
set { _DataEditor = value; NotifyPropertyChanged("DataEditor"); }
}
...
}
DataGrid columns and VM is built totally dynamically from code.
I create template column (DataGridTemplateColumn) and set the CellTemplate to the followind template (illustrated through XAML):
<StackPanel>
<TextBlock Text="{Binding Path=DataValue}" />
<ContentControl Content="{Binding Path=DataEditor}" />
</StackPanel>
Everything works fine and as expected when DataGrid is initially filled and shown.
Now, If I try to SCROLL the grid the following exception occurs:
Specified element is already the logical child of another element.
Disconnect it first.
This exception has something to do with grid's row virtualization and binding Content to DataEditor inside cell template. If I turn off the row virtualization, everything works fine but grid performance becomes very bad so it is not an option.
Do you know DataGrid virtualization works behind the scenes, what happens when row is loaded/unloaded and what could be causing this error?
Are there any workarounds?
Note: I cannot use ContentTemplate binding for cell data editor in cell template (which is suggested in many places as workaround) because I MUST manually create and initialize this cell editor.
Solved.
This is obviously a bug somewhere in ContentControl when using Content binding in combination with DataGrid virtualization.
Once I switched to ContentPresenter everything works flawlessly.
Working cell template is:
<StackPanel>
<TextBlock Text="{Binding Path=DataValue}" />
<ContentPresenter Content="{Binding Path=DataEditor}" />
</StackPanel>

XAML: Update binding when changing DataContext

I have a simple XAML file, it contains a Label whose Foreground property contains a binding:
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="200" Height="100" >
<Label Content="Sampletext" Foreground="{Binding Path=Color}" />
</Grid>
When I load the template and apply a DataContext the Foreground still has the default value.
Is it possible to get bound foreground value without rendering the Grid?
// Load template
string templatePath = "/WpfApplication1;component/Template.xaml";
Grid grid = Application.LoadComponent(new Uri(templatePath, UriKind.Relative)) as Grid;
// Set dataContext
grid.DataContext = new { Color = Brushes.Green };
// Foregound still has default value
var foreground = ((Label)grid.Children[0]).Foreground;
Project can be downloaded here: http://dl.dropbox.com/u/21096596/WpfApplication1.zip
try
lblName.GetBindingExpression(Label.ForegroundProperty).UpdateTarget();
before
var foreground = ((Label)grid.Children[0]).Foreground;
There are automatic DataContext change notifications, the binding will update if the necessary conditions are met. One of them is that the control is loaded (IsLoaded == true) which is not the case in your code. The control will only load if you add it to the your UI somewhere.
Example test code:
private void Button_Click(object sender, RoutedEventArgs e)
{
Grid grid = null;
Action action = () =>
{
var foreground = ((Label)grid.Children[0]).Foreground;
MessageBox.Show(foreground.ToString());
grid.DataContext = new { Color = Brushes.Green };
foreground = ((Label)grid.Children[0]).Foreground;
MessageBox.Show(foreground.ToString());
};
grid = Application.LoadComponent(new Uri("Stuff/GridOne.xaml", UriKind.Relative)) as Grid;
if (grid.IsLoaded)
{
action();
}
else
{
grid.Loaded += (s, _) => action();
}
// This adds the grid to some StackPanel, if you do not do something like this
// nothing will happen since the control will not be loaded and thus the event
// will not fire, etc.
ControlStack.Children.Add(grid);
}
Why do you need the onetime binding? remove that, and it should work.
Wrap your DataContext in an object, and implement INotifyPropertyChanged, then the binding will update when the property changes, and there's no need to update the binding manually:
public class MyDataContext : INotifyPropertyChanged
{
private Brush color;
public Brush Color
{
get { return color; }
set
{
color = value;
RaisePropertyChanged("Color");
}
}
//implementation of PropertyChanged and RaisePropertyChanged omitted
}
and then update it like so:
var dc = new MyDataContext();
grid.DataContext = dc;
dc.Color = Brushes.Green; //this will trigger the NotifyPropertyChanged and update the binding
//color should be changed now
var foreground = ((Label)grid.Children[0]).Foreground;
Hopefully this helps...
If you want a property of a control to be binding to a property of DataContext but want to change the datacontext in runtime, there is a much more simple way to do it.
Create a ContentControl, then use ContentControl.ContentTemplate
<ContentControl Content=something>
<ContentControl.ContentTemplate>
<DataTemplate>
<Label Foreground="{Binding Path=Color}" />
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
Change the Content of the ContentControl instead of changing DataContext of the Label.

Adding IValueConverter breaks updates when changed occur from observable collection to UI

I have a itemscontrol with the item template set to a border, then i bind the datacontext of the listview to a list of objects that contain a bool property.
Then i added a click event handler to the border, and when detecting a click, i cast the datacontext of the border to the class and set the bool field to true.
That works as a charm, but i want the rectangle to have a specific colour when the bool field is set to true or false, so i created a IValueConverter that takes my class and returns a colour.
That works too, the rectangles are different colors based on the bool field.
I am still able to click the rectangles, but they just arent updated.
The color of the rectangle wont change.
Datatemplate from the itemscontrol itemtemplate
<DataTemplate>
<Border ToolTip="{Binding Seat.Column}" Width="25" Height="25" Margin="0,0,2,2" BorderBrush="Black" Background="{Binding Converter={StaticResource ResourceKey=SeatStateConverter}}" BorderThickness="2" Name="rectangle1" VerticalAlignment="Center" HorizontalAlignment="Center" MouseLeftButtonDown="rectangle1_MouseLeftButtonDown">
<Label Content="{Binding Occupied}" Foreground="White" FontSize="7"></Label>
</Border>
</DataTemplate>
The click event
private void rectangle1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Border item = sender as Border;
SeatState state = item.DataContext as SeatState;
state.Locked = !state.Locked;
}
my converter
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
SeatState state = value as SeatState;
if (state == null)
return null;
SolidColorBrush brush = new SolidColorBrush();
if (state.Occupied)
{
brush.Color = Color.FromRgb(172, 0,0);
}
else if (state.Locked)
{
brush.Color = Color.FromRgb(214, 65, 0);
}
else if(!state.Occupied)
{
brush.Color = Color.FromRgb(0, 172, 0);
}
return brush;
}
This works great.. untill i add the converter that converts the objects into a SolidColorBrush.
I tried all sorts of crazy stuff that should have nothing to do with my problem.
implementing the convertback method
in the IValueConverter interface
setting the binding to a two way binding
invoking the UpdateLayout
method on the ItemsControl
But nothing seemed to work.
Anyone got any ideas?
My english could be better so please ask if there is anything you want clearified =)
Thanks in advance.
I think you are binding to the SeatState object - whereas you actually need to bind to some combination of the Occupied an Locked properties?
i.e. it's not the SeatState object itself that is changing, but rather its a couple of the properties of the SeatState.
Maybe merge the properties together somehow and set this merged property as the Path for the XAML Background.
e.g. within SeatState
private bool _Locked
public bool Locked
{
get
{
return _Locked;
}
set
{
_Locked = value;
NotifyPropertyChange("Locked");
NotifyPropertyChange("LockedAndOccupied");
}
}
private bool _Occupied
public bool Occupied
{
get
{
return _Occupied;
}
set
{
_Occupied = value;
NotifyPropertyChange("Occupied");
NotifyPropertyChange("LockedAndOccupied");
}
}
public Tuple<bool, bool> LockedAndOccupied
{
get
{
return new Tuple<bool, bool>(Locked, Occupied);
}
}
then in the XAML you can bind to Path=LockedAndOccupied, Converter=...
Obviously you'll have to change the Converter code too - I'll let you do that!
Alternatively... now I've read up about it...
There is something called a MultiBinding - http://www.switchonthecode.com/tutorials/wpf-tutorial-using-multibindings - looks perfect for your needs
Something like:
<Border.Background>
<MultiBinding Converter="{StaticResource aNewConverter}">
<Binding Path="Locked"/>
<Binding Path="Occupied"/>
</MultiBinding>
</Border.Background>
I've learnt something new tonight :)
Check the Background binding... it looks like your Path is missing. I would expect to see something like...
Background="{Binding Path=., Converter={StaticResource ResourceKey=SeatStateConverter}}"
Alternately you could try setting BindsDirectlyToSource=true.
On second thought, you probably need to implement an IMultiValueConverter, and then bind each of the properties separately. That may be what you need to do to get the change notifications on each of the properties. Here is an example of an IMultiValueConverter implementation from MSDN.
Also, you may want to check your implementation of INotifyPropertyChanged... misspellings of property names will break the change notifications...

Categories

Resources