DependencyProperty binding error, programmatically - c#

What I have is a well-working C# and XAML code, which does exactly what it is supposed to do, well, almost exactly. I am trying to make my custom, working, DependencyProperty for UserControl - and it is made, well-formed and supposedly working. There are two properties: SumOfApproximationsProperty and SumOfPositionsProperty. These getters and setters simply do not get invoked on certain actions - and this is my problem. They are declared in this UserControl class:
public partial class PresentationCell : UserControl
{
public Label SumOfApproximations;
public Label SumOfPositions;
public PresentationCell()
{
InitializeComponent();
DataContext = this;
this.MinHeight = 40;
this.MinWidth = 40;
SumOfApproximations = this.SumOfApproximation;
SumOfPositions = this.SumOfPosition;
}
public static readonly DependencyProperty SumOfApproximationsProperty =
DependencyProperty.Register("AproximationsProperty", typeof(String),
typeof(PresentationCell), new UIPropertyMetadata(null));
public static readonly DependencyProperty SumOfPositionsProperty =
DependencyProperty.Register("PositionsProperty", typeof(String),
typeof(PresentationCell), new UIPropertyMetadata(null));
public String AproximationsProperty
{
get { return (String)GetValue(SumOfApproximationsProperty); }
set { SetValue(SumOfApproximationsProperty, value); }
}
public String PositionsProperty
{
get { return (String)GetValue(SumOfPositionsProperty); }
set { SetValue(SumOfPositionsProperty, value); }
}
}
As You can see, it is composed of two Labels, that have their own text-setting properties. And here's this UserControl XAML:
// USER CONTROL XAML
<UserControl x:Class="PodstawyModelowaniaISymulacjiRozmytej.Controls.PresentationCell"
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"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*">
</ColumnDefinition>
<ColumnDefinition Width="2*">
</ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="1*">
</RowDefinition>
<RowDefinition Height="2*">
</RowDefinition>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Label Name="SumOfApproximation" Content="{Binding Path=AproximationsProperty}">
</Label>
</Grid>
<Grid Grid.Row="1">
</Grid>
</Grid>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="1*">
</RowDefinition>
<RowDefinition Height="2*">
</RowDefinition>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
</Grid>
<Grid Grid.Row="1">
<Label Name="SumOfPosition"></Label>
</Grid>
</Grid>
</Grid>
</UserControl>
This UserControl is going to be used with DataGrid (as it's cells), which is declared below (in XAML):
// MAIN WINDOW DATAGRIG DECLARATION MAINWINDOW.XAML
<Grid Grid.Row="2" Name="DataThree_Grid">
<DataGrid Name="ResultData_DataGrid" HeadersVisibility="Row" Margin="5 5 5 5"></DataGrid>
</Grid>
Here's the code, that prepares and creates a column in this DataGrid, filled with PresentationCell UserControls:
// MAIN WINDOW CREATE COLUMN FOR DATAGRID FUNCTION MAINWINDOW.XAML.CS
private DataGridTemplateColumn CreatePresentationTemplateColumn(Binding positions, Binding aproximations)
{
DataGridTemplateColumn doubleOnlyTextBoxColumn = new DataGridTemplateColumn();
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(PresentationCell));
DataTemplate dataTemplate = new DataTemplate();
factory.SetValue(PresentationCell.SumOfApproximationsProperty, aproximations);
factory.SetValue(PresentationCell.SumOfPositionsProperty, positions);
dataTemplate.VisualTree = factory;
doubleOnlyTextBoxColumn.CellTemplate = dataTemplate;
return doubleOnlyTextBoxColumn;
}
Other code, that can be deemed useful for You to answer this question:
// MAIN WINDOW INITIALIZING BUTTON MAINWINDOW.XAML.CS
private void SubtractionLR_Button_Click(object sender, RoutedEventArgs e)
{
MyData[] table = new MyData[]
{
new MyData
{
Values = new element[2]
{
new element
{
var1 = 7,
var2 = 6
},
new element
{
var1 = 4,
var2 = 1
}
}
},
new MyData
{
Values = new element[2]
{
new element
{
var1 = 67,
var2 = 3
},
new element
{
var1 = 44,
var2 = 1
}
}
}
};
fillPresentationDataGrid(ResultData_DataGrid, table);
}
Now, after all of the code has been described, the problem lingers here. As You can see, I am trying to create Binding object for my column of PresentationCell UserControls. The problem is, that this String in this Binding is rather unknown for me - its specification and so on. As a result, program cannot find data that should be provided to my control (and for its labels) through this binding. The data should come from MyData[] table. Program shows an error about "cannot find Values" etc. and the cells in DataGrid are blank.
// MAIN WINDOW FILLING PRESENTATION GRID FUNCTION MAINWINDOW.XAML.CS
private void fillPresentationDataGrid(DataGrid dataGrid, MyData[] table)
{
dataGrid.AutoGenerateColumns = false;
for (int i = 0; i < table[0].Values.Length; i++)
{
DataGridTemplateColumn col = CreatePresentationTemplateColumn(new Binding("Values[" + i + "].var1"), new Binding("Values[" + i + "].var2"));
dataGrid.Columns.Add(col);
}
dataGrid.ItemsSource = table;
}
EDIT
All I want is to get that MyData[] table content displayed on DataGrid control using my own custom UserControl. When I change that factory.SetValue(PresentationCell.SumOfApproximationsProperty, aproximations); into factory.SetValue(PresentationCell.SumOfApproximationsProperty, "foo");, the DataGrid will display "foo"'s.
EDIT2
Unfortunately, the problem still exists.

In the constructor of PresentationCell you set this.DataContext = this.
By setting DataContext to your control you are breaking the inheritance of this property and thats why setting the bindings in CreatePresentationTemplateColumn wont work.
To fix that you can remove this line and bind the controls by RelativeSource/ElementName or you can set the dataContext to the main grid in PresentationCell instead of the root level

Related

How to add a XAML Resource (Grid with ItemsControls) to a FlowDocument in Code-Behind multiple times

I am working on a WPF Window that presents the results of some technical calculations inside a FlowDocumentViewer.
Problem: The FlowDocument and all its content is created from Code-Behind because every calculation differs a bit in terms of headers, shown results, and lines. I use different BlockUIContainers which hold a Resource Grid with some ItemsControls to show the results in a formatted order and add it to a Section and then to the Blocks of the Document, but only the last Block is shown inside the Reader.
I don't understand why after using FlowDoc.Blocks.Add(section) multiple times, only the last Block is shown.
I have created a Resource inside the XAML Code to fill it from Code Behind with the results. I need the output to look like this, e.g.:
M,ed = 70 kNm
Q,ed = 25 kN
N,ed = 30 kN
...
To achieve formatting like this, I created a grid with three columns, each containing an ItemsControl with a DataTemplate TextBlock, which has a Binding to a List<> of results.
<Window x:Class="FaceplateInput.Output"
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:FaceplateInput"
mc:Ignorable="d"
x:Name="OutputWnd"
WindowStartupLocation="CenterScreen"
Title="Output" Height="1000" Width="700">
<Window.Resources>
<Grid x:Key ="TestGrid" x:Name="Grid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ItemsControl x:Name="ItemsLeft" Grid.Column="0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl x:Name="ItemsMid" Grid.Column="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl x:Name="ItemsRight" Grid.Column="2">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window.Resources>
...
<FlowDocumentScrollViewer x:Name="DocReader" Grid.Column="1" Grid.Row="2" Margin="20" MaxWidth="700">
</FlowDocumentScrollViewer>
</Grid>
</Window>
I want to create the FlowDocument in Code-Behind to have control over formatting, especially because the document is created from a single string that is returned by the calculating class.
I build the following test-method to see how FlowDocuments work. The result is that only the added paragraphs and the last section are shown in the output window.
Code-Behind:
namespace FaceplateInput
{
public partial class Output: Window
{
public string PathToImage;
public string PathToBackground;
public string PathToTXTFile;
public Output()
{
InitializeComponent();
CreateFlowDocument();
}
private void CreateFlowDocument()
{
FlowDocument FlowDoc = new FlowDocument();
List<string> leftStr = new List<string>();
List<string> midStr = new List<string>();
List<string> rightStr = new List<string>();
//for Testing purposes, i create some weired data to fill the Lists
for (int i = 0; i < 10; i++)
{
leftStr.Add($"LeftLine {i + 1}");
midStr.Add("=");
rightStr.Add($"RightLine {i + 1}");
}
//Creating a new Container with Resource Grid as UIElement:
BlockUIContainer cont = new BlockUIContainer((UIElement)this.FindResource("TestGrid"));
Grid child = (Grid)cont.Child;
//setting the sources for the ItemsControl:
ItemsControl items1 = (ItemsControl)child.Children[0];
items1.ItemsSource = leftStr;
ItemsControl items2 = (ItemsControl)child.Children[1];
items2.ItemsSource = midStr;
ItemsControl items3 = (ItemsControl)child.Children[2];
items3.ItemsSource = rightStr;
//adding section holding BlockUIContainer to Document
Section section = new Section();
section.Blocks.Add(cont);
FlowDoc.Blocks.Add(section);
//disconnecting UIContainer from parent to avoid Exception
section.RemoveChild(cont.Child);
cont.Child = null;
//another "testline" to see where it puts it in the document
FlowDoc.Blocks.Add(new Paragraph(new Run("TestString 1\n")));
//all the stuff above again to test
leftStr.Clear();
midStr.Clear();
rightStr.Clear();
leftStr.Add("______");
midStr.Add("_____________________");
rightStr.Add("_____________________");
for (int i = 0; i < 20; i++)
{
leftStr.Add($"Left: {(double)i * 324 / 10}\n");
midStr.Add("=\n");
rightStr.Add($"{(double)i * 13 / 2}\n");
}
BlockUIContainer cont1 = new BlockUIContainer((UIElement)this.FindResource("TestGrid"));
Grid child1 = (Grid)cont1.Child;
items1 = (ItemsControl)child1.Children[0];
items1.ItemsSource = leftStr;
items2 = (ItemsControl)child1.Children[1];
items2.ItemsSource = midStr;
items3 = (ItemsControl)child1.Children[2];
items3.ItemsSource = rightStr;
Section section1 = new Section();
section1.Blocks.Add(cont1);
FlowDoc.Blocks.Add(new Paragraph(new Run("TestString 2\n")));
FlowDoc.Blocks.Add(section1);
FlowDoc.Blocks.Add(new Paragraph(new Run("TestString 3\n")));
DocReader.Document = FlowDoc;
}
//...
}
}
I'd appreciate any tips on how to get this working, and maybe how to improve the code itself, or any ideas for completely different approaches.
Greets and thx, Crawliiee
You are setting the first container to null, so that first section is empty. Remove this line:
cont.Child = null;
An alternative could be to use a ListView with GroupView as in the example
https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.listview?view=netframework-4.7.2&f1url=%3FappId%3DDev16IDEF1%26l%3DEN-US%26k%3Dk(System.Windows.Controls.ListView);k(TargetFrameworkMoniker-.NETFramework,Version%253Dv4.7.2);k(DevLang-csharp)%26rd%3Dtrue

C# with WPF - Creating a grid of buttons resizable at runtime

I want to implement the following: I have an empty window in the beginning with 3 buttons. When I click a button, I want to generate Size*Size buttons in the window. For button 1, Size=6, for button 2 Size=8 and for button 3 Size=0, so I thought I'd create a UniformGrid and bind its size to Size, so I can change the number of buttons present. Initially, Size would be 0, so no buttons can be seen, then when Size changes, the buttons appear. This, however, doesn't work. I'm trying:
<Window x:Class="project.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="500" Width="700">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Menu Grid.Column="1" Margin="38,0,187,430" Background="White">
<MenuItem Header="Level 1" FontFamily="Roboto" Height="32" Width="65"
Command="{Binding Lvl1Command}"/>
<MenuItem Header="Level 2" FontFamily="Roboto" Height="32" Width="65"
Command="{Binding Lvl2Command}"/>
<MenuItem Header="Level 3" FontFamily="Roboto" Height="32" Width="65"
Command="{Binding Lvl3Command}"/>
</Menu>
<ItemsControl Grid.Column="2" ItemsSource="{Binding Fields}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="{Binding Size}" Columns="{Binding Size}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Focusable="False" RenderTransformOrigin="0.5, 0.5"
Width="30" Height="25" FontSize="24" FontWeight="Bold">
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
Size is initially 0, Lvl1Command changes Size to 6, Lvl2Command to 8 etc. Fields is just a data structure storing some properties that will affect the style of the button. How could/should I modify this so that when Size changes, the number of appearing Buttons does too? Thank you!
EDIT
In the ViewModel constructor:
Lvl1Command = new DelegateCommand(param => { SetUpGame(MLModel.Level.Easy); });
Lvl2Command = new DelegateCommand(param => { SetUpGame(MLModel.Level.Medium); });
Lvl3Command = new DelegateCommand(param => { SetUpGame(MLModel.Level.Hard); });
And SetUpGame() looks like this (Field included):
private void SetUpGame(MLModel.Level level)
{
UpCommand = new DelegateCommand(param => { _model.MoveUp(); RefreshTable(); });
DownCommand = new DelegateCommand(param => { _model.MoveDown(); RefreshTable(); });
LeftCommand = new DelegateCommand(param => { _model.MoveLeft(); RefreshTable(); });
RightCommand = new DelegateCommand(param => { _model.MoveRight(); RefreshTable(); });
// időzítő létrehozása
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += new EventHandler(Timer_Tick);
_timer.Start();
_model.SetLevel(level);
_model.NewGame();
Fields = new ObservableCollection<MLField>();
for (Int32 i = 0; i < _model.Table.Size; i++)
{
for (Int32 j = 0; j < _model.Table.Size; j++)
{
Fields.Add(new MLField
{
Text = _model.Table[i, j],
X = i,
Y = j,
Number = i * _model.Table.Size + j
});
}
}
RefreshTable();
}
And then Size:
public Int32 Size { get { return _model.Size; } }
The ViewModel must implement INotifyPropertyChanged. Add this code:
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Then, if you want to raise notifications for changes on the Size property, write it this way:
public int Size
{
get { return _Size; }
set
{
if (_Size != value)
{
_Size = value;
NotifyPropertyChanged();
}
}
}
private int _Size;
Also, the Fields collection is initially null, and when you instantiate it in the SetupGame, no notification is raised so the View is still bound to the null reference. You have 2 options:
1) initialize the Fields collection in the constructor. This way, when the ViewModel is passed to the View, the collection is ready to be bound to the ItemsControl. It is not necessary to fill the collection in the constructor, just to instantiate it.
2) implement the Fields property in the same way the Size is:
public ObservableCollection<MLField> Fields
{
get { return _Fields; }
set
{
if (_Fields != value)
{
_Fields = value;
NotifyPropertyChanged();
}
}
}
private ObservableCollection<MLField> _Fields;
This way, you can set a new instance of the collection every time you want, and the ItemsControl binding will update consequently.

Programmatically created controls not rendering

As I was required to sort of mask input in a textbox, I decided to construct my own control to handle this.
One of many templates could be "Size {enter size} Colour {enter colour}" which I've broken down to create a series of controls. The custom control that extends StackPanel which I've named CustomTextBox generates the following from the constructor.
// Pseudo
Children = {
Label = { Content = "Size" },
TextBox = { Text = "enter size" },
Label = { Content = "Colour" },
TextBox = { Text = "enter colour" }
// .. and an arbitrary amount of more Labels and TextBoxes in no particular order
}
So far so good. But when I want it to render.. That's where my headache starts.
I've tried to add the controls to the Children property and Measure/Arrange on the parent, itself and all the Children. ActualHeight and ActualWidth do change to something other than 0, but they won't render/display/become visible whatsoever.
I've also tried to use an ItemsControl and add the controls to the ItemsSource property to no avail.
I've tried to predefine sizes on everything, colour the background red and all, but the elusive controls remain to be caught and tied to my screen.
There's got to be a huge "Oooh..." here that I just can't find. I refuse to believe that this can't be done. I mean, it's WPF. WPF is awesome.
Edit Updated to what I currently have that seems most likely to work - still doesn't though.
Whatever I do in the designer shows up, but nothing I do in the CustomTextBox makes any visible difference.
Edit
New headline that fits the problem better.
Also, I've found several examples of programmatically adding controls. Take this article for example. I fail to see the difference between my scenario and theirs, except that theirs work and the buttons are visible.
Update3
The mistake was to assume, that one can simply replace control in visual tree by assigning in codebehind a new control to it's name (specified in xaml)
Updated2
Your mistake was following. If you write
<TextBlock Name="tb" Text="tb"/>
and then in code you will do
tb = new TextBlock() { Text = "Test" };
then you will have a new textblock as a variable, and nothing in xaml will change. You either have to change existing control, or remove old control and add new.
I'm talking about your Headline, Subtext & Description. You don't change them
Updated:
Here is an example of dynamically creating controls by specifying input mask:
MainWindow.xaml
<Window x:Class="WpfApplication35.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" xmlns:local="clr-namespace:WpfApplication35">
<Grid>
<local:UserControl1 x:Name="myUserControl"/>
</Grid>
</Window>
MainWindow.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
myUserControl.BuildControls("a {enter a} b {enter b1}{enter c2}");
}
}
UserControl1.xaml
<UserControl x:Class="WpfApplication35.UserControl1"
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"
d:DesignHeight="30" d:DesignWidth="300">
<WrapPanel Name="root" Orientation="Horizontal"/>
</UserControl>
UserControl1.cs
public partial class UserControl1 : UserControl
{
public List<CustomField> Fields = new List<CustomField>();
public UserControl1()
{
InitializeComponent();
}
public UserControl1(string mask)
{
InitializeComponent();
BuildControls(mask);
}
public void BuildControls(string mask)
{
//Parsing Input
var fields = Regex.Split(mask, #"(.*?\}\s)");
foreach (var item in fields)
{
if (item != "")
{
int index = item.IndexOf('{');
string namestring = item.Substring(0, index).Trim();
var field = new CustomField() { Name = namestring };
string valuesstring = item.Substring(index, item.Length - index).Trim();
var values = valuesstring.Split(new char[] { '{', '}' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var val in values)
{
var valuewrapper = new FieldValue() { Value = val };
field.Values.Add(valuewrapper);
}
Fields.Add(field);
}
}
foreach (var field in Fields)
{
var stackPanel = new StackPanel() { Orientation = Orientation.Horizontal };
var label = new Label() { Content = field.Name, Margin = new Thickness(4) };
stackPanel.Children.Add(label);
foreach (var item in field.Values)
{
var tb = new TextBox() { Margin = new Thickness(4), Width = 200 };
tb.SetBinding(TextBox.TextProperty, new Binding() { Path = new PropertyPath("Value"), Source = item, Mode = BindingMode.TwoWay });
stackPanel.Children.Add(tb);
}
root.Children.Add(stackPanel);
}
}
}
public class CustomField
{
public string Name { get; set; }
public List<FieldValue> Values = new List<FieldValue>();
}
public class FieldValue
{
public string Value { get; set; }
}
This way fields and values are gonna be represented by Fields collection in UserControl1. Values of fields are updated as user types something. But only one-way, i.e. user input updates corresponding Value property, but changing Value property at runtime will not affect corresponding textbox. To implement updating from Value to textbox you have to implement INotifyProperty interface
Obsolete
Since you've asked.
There are hundreds of possible implementations, depending on what are you trying to archieve, how do you want validation to be, do you want to use MVVM, do you want to use bindings etc. There are generally 2 approaches : creating usercontrol and creating custom control. First one suits you better I believe.
Create a usercontrol with following xaml:
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Content="Size: " Grid.Column="0"/>
<TextBox Name="tbSize" Grid.Column="1"/>
<Label Content="Colour:" Grid.Column="2"/>
<TextBox Name="tbColour" Grid.Column="3"/>
</Grid>
In code-behind you can access TextBoxes by their name and do whatever you want to do.
You can use usercontrol in both xaml and codebehind.
In xaml:
Specify alias for namespace of your usercontrol (look at xmlns:local)
<Window x:Class="WpfApplication35.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
xmlns:local="clr-namespace:WpfApplication35">
<Grid>
<local:UserControl1/>
</Grid>
</Window>
In codebehind you can use it like this:
public MainWindow()
{
InitializeComponent();
var myUserControl = new UserControl1();
}
There is a lot to say and these are basic things, so check tutorials and ask questions.
P.S. If you are learning WPF it's mandatory to learn bindings.

Single-line wpf textbox horizontal scroll to end

I have a templated listbox which template among other things contains a wpf textbox too. The data is provided to the listbox through ItemsSource.
The textboxes display filepaths and these are usally quite long. I want when the textboxes are loaded to show the end of the filepaths.
I tried a combination of DataContextChanged event and setting HorizontalScrollBarVisibility (using double.max or getting the real char length) but to no success. The DataContextChanged seems to be the correct event to use as it fires on each setting of the ItemsSource.
Edit:
Here is sample code to show when the suggestion by Lester works and when it doesnt. I am trying to have it work when the text is set through binding.
<Window x:Class="WpfAppTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
Loaded="LoadedHandler">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<TextBox Name="tbb" Width="50" Height="20" Text="{Binding Path=Str}"
IsReadOnly="True" Grid.Column="0" Grid.Row="0"
DataContextChanged="ContextChangedHandler"/>
<ListBox SelectionMode="Single" x:Name="listBox" Grid.Column="0" Grid.Row="1"
VerticalAlignment="Top">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Width="50" Height="20" Text="{Binding Path=Str}"
IsReadOnly="True"
DataContextChanged="ContextChangedHandler"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var obj = new SomeClass
{
Str = "qwetyuiuropqo[psdal;dkas;ldamzxn m,cnz128391"
};
listBox.ItemsSource = new List<SomeClass> { obj };
tbb.DataContext = obj;
}
public class SomeClass
{
public string Str { get; set; }
}
private void LoadedHandler(object sender, RoutedEventArgs e)
{
var obj = new SomeClass
{
Str = "qwetyuiuropqo[psdal;dkas;ldamzxn m,cnz128391"
};
listBox.ItemsSource = new List<SomeClass> { obj };
tbb.DataContext = obj;
}
private void ContextChangedHandler(object sender, DependencyPropertyChangedEventArgs e)
{
var textBox = sender as TextBox;
if (textBox == null) return;
textBox.CaretIndex = textBox.Text.Length;
var rect = textBox.GetRectFromCharacterIndex(textBox.CaretIndex);
textBox.ScrollToHorizontalOffset(rect.Right);
}
}
This code worked for me for scrolling to the end of the TextBox (taken from this question):
textBox.CaretIndex = textBox.Text.Length;
var rect = textBox.GetRectFromCharacterIndex(textBox.CaretIndex);
textBox.ScrollToHorizontalOffset(rect.Right);
Solution is to change DataContextChanged event with Loaded so that proper notifications are received for the textbox.

Default datacontext

Having the xaml below in MainWindow.xaml:
<Window x:Class="TestDependency.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Label Name="someLabel" Grid.Row="0" Content="{Binding Path=LabelText}"></Label>
<Button Grid.Row="2" Click="Button_Click">Change</Button>
</Grid>
</Window>
And the following code behind in MainWindow.xaml.cs:
public static readonly DependencyProperty LabelTextProperty = DependencyProperty.Register("LabelText", typeof(String), typeof(MainWindow));
public int counter = 0;
public String LabelText
{
get
{
return (String)GetValue(LabelTextProperty);
}
set
{
SetValue(LabelTextProperty, value);
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
LabelText = "Counter " + counter++;
}
I would have thought that the default DataContext is the code behind. But I'm forced to specify the DataContext. Which DataContext is the default? Null? I would have thought that the code behind would have been (as is the same class).
And as in this sample I'm using the code behind to modify the content of the Label, could I use directly:
someLabel.Content = "Counter " + counter++;
I will expect that being the code behind, it shouldn't have the UI update problem that you have if the DataContext is in a different class.
Yes, the default value of DataContext is null, here is how it's declared in FrameworkElement class -
public static readonly DependencyProperty DataContextProperty =
DependencyProperty.Register("DataContext", typeof(object),
FrameworkElement._typeofThis,
(PropertyMetadata) new FrameworkPropertyMetadata((object)null,
FrameworkPropertyMetadataOptions.Inherits,
new PropertyChangedCallback(FrameworkElement.OnDataContextChanged)));
FrameworkPropertyMetadata takes first param for default Value of property.
As it gets inherited by all the child controls your lable's DataContext remains null unless you specify the window data context.
and you can use someLabel.Content = "Counter " + counter++; in codebehind to set labels content; as such it's perfectly fine to access your controls in code behind.
Since you are binding a property of a Label, unless you specify a different binding source somehow the binding engine assumes that LabelText is a property on that class. It cannot magically determine that because the Label is a descendant of a MainWindow the binding source should be that window, which is why you need to explicitly declare it.
It's important to note that the concepts of "data context" and "binding source" are distinct: DataContext is one way to specify the binding source, but there are also others.

Categories