WPF ScrollViewer not working in Grid programmatically populated - wpf

I have this situation in xaml
<StackPanel Orientation="Vertical" Margin="5" Background="AliceBlue" Height="370">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid Name="DescriptionsGrid" MinHeight="0" MaxHeight="370" ScrollViewer.CanContentScroll="True"></Grid>
</ScrollViewer>
</StackPanel>
and I'm filling the Grid with the following
DescriptionsGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
TextBlock textBlock = new TextBlock { Text = description, TextWrapping = TextWrapping.WrapWithOverflow };
Label label = new Label { Content = textBlock, Foreground = new SolidColorBrush(Colors.Blue) };
label.MouseLeftButtonDown += new MouseButtonEventHandler(LoadXML);
DescriptionsGrid.Children.Add(label);
Grid.SetRow(DescriptionsGrid.Children[DescriptionsGrid.Children.Count - 1], description_counter);
description_counter++;
I can't define rows height as the description may be long enought to wrap the text to new line.
The scrollbar does not appears and new elements go hidden down below.
Any idea?

Following the suggestion of #Clemens I rewrote the code, and found that the mistake was in the XML.
To populate the grid
foreach (HostMessages hostMessage in hostMessagesList)
{
Label message = new Label
{
Content = new TextBlock {
Text = hostMessage.Description,
TextWrapping = TextWrapping.WrapWithOverflow
},
Foreground = new SolidColorBrush(Colors.Blue),
Cursor = Cursors.Hand
};
message.MouseLeftButtonDown += new MouseButtonEventHandler(LoadXML);
itemsControl.Items.Add(message);
}
The XML is as follows
<DockPanel Grid.Row="1" Margin="5" Background="AliceBlue" Height="370">
<ScrollViewer VerticalScrollBarVisibility="Auto" Height="370">
<ItemsControl Name="itemsControl"></ItemsControl>
</ScrollViewer>
</DockPanel>
The mistake was assigning a MaxHeight="370" to the Grid and none to the ScrollViewer.

Related

WPF How to find a specific control in an ItemsControl with data binding

I have an ItemsControl which is bound to a list:
<ItemsControl x:Name="icFiles" ItemsSource="{Binding Path=files}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Content="" IsChecked="{Binding IsChecked, Mode=TwoWay}" />
<TextBlock x:Name="ThisTextBlock" Text="{Binding FileName}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
private readonly List<FileModel> files = new();
icFiles.ItemsSource = files;
I want to highlight certain text in the TextBlock in the ItemsControl. For this, I thought about using a TextPointer:
string? highlightText = "blue";
int highlightTextIndex = ThisTextBlock.Text.IndexOf(highlightText);
if(highlightTextIndex >= 0)
{
TextPointer textStartPointer = ThisTextBlock.ContentStart.DocumentStart.GetInsertionPosition(LogicalDirection.Forward);
TextRange? highlightTextRange = new TextRange(textStartPointer.GetPositionAtOffset(highlightTextIndex), textStartPointer.GetPositionAtOffset(highlightTextIndex + highlightText.Length));
highlightTextRange.ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.Blue);
}
}
How do I find this ThisTextBlock?
You need to access the item container's content template (which is the item's DataTemplate).
In case of the ItemsControl, you can use the following example to obtain a named element from the DataTemplate:
for (int itemIndex = 0; itemIndex < this.ItemsControl.Items.Count; itemIndex++)
{
var itemContainer = this.ItemsControl.ItemContainerGenerator.ContainerFromIndex(itemIndex) as ContentPresenter;
var textBlock = itemContainer.ContentTemplate.FindName("ThisTextBlock", itemContainer) as TextBlock;
HighlightText(textBlock);
}
A simple implementation that searches an element in the visual tree can be found at How to: Microsoft Docs: How to: Find DataTemplate-Generated Elements. You can copy and use the example's helper method FindVisualChild to search for elements by type rather than by name. The method is part of an example that shows how to get the content of the DataTemplate in case you use a ListBox or ListView.
In case you didn't modified the ListBoxItem template or don't expect it to change, you can use this simplified and faster version (to find named elements):
for (int itemIndex = 0; itemIndex < this.ListBox.Items.Count; itemIndex++)
{
var listBoxItemContainer = this.ListBox.ItemContainerGenerator.ContainerFromIndex(itemIndex) as ListBoxItem;
var templateRootBorder = VisualTreeHelper.GetChild(listBoxItemContainer, 0) as Border;
var contentHost = templateRootBorder.Child as ContentPresenter;
var textBlock = contentHost.ContentTemplate.FindName("TD", contentHost) as TextBlock;
}
Except for special use cases, it is highly recommended to use the ListBox instead of the ItemsControl. ListBox and ListView are both an extended ItemsControl. They both provide scrolling and a significantly improved performance.
First of all, you need to delete the Binding from code behind.
You can do this using Loaded event as follows:
<ItemsControl x:Name="icFiles" ItemsSource="{Binding Path=files}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Content="" IsChecked="{Binding IsChecked, Mode=TwoWay}" />
<TextBlock Loaded="ThisTextBlock_OnLoaded" x:Name="ThisTextBlock" Text="{Binding FileName}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
private void ThisTextBlock_OnLoaded(object sender, RoutedEventArgs e)
{
if (sender is TextBlock tb)
{
string? highlightText = "blue";
int highlightTextIndex = tb.Text.IndexOf(highlightText);
if (highlightTextIndex >= 0)
{
TextPointer textStartPointer = tb.ContentStart.DocumentStart.GetInsertionPosition(LogicalDirection.Forward);
TextRange? highlightTextRange = new TextRange(textStartPointer.GetPositionAtOffset(highlightTextIndex), textStartPointer.GetPositionAtOffset(highlightTextIndex + highlightText.Length));
highlightTextRange.ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.Blue);
}
}
}

Access the TextBlock 's text at run time in DataTemplate of ItemsControl in WPF

Iam displaying messages in my WPF application
when a new message is added to the messages, i need to highlight it.so i want to dynamically get the text added to TextBlock
i have the xaml like this
<ItemsControl Name="DialogItemsControl" ItemsSource="{Binding Messages, Mode=OneWay}" Background="Transparent"
BorderBrush="Transparent" TargetUpdated="DialogItemsControl_TargetUpdated">
<ItemsControl.ItemTemplate><!-- For ever message -->
<DataTemplate>
<Grid Margin="0,0,0,20">
<ItemsControl Name="SubDialogItemsControl"
Foreground="{DynamicResource ButtonTextBrush}"
ItemsSource="{Binding Lines,NotifyOnTargetUpdated=True}"
Margin="0,0,0,12"
Grid.Column="0">
<ItemsControl.ItemTemplate><!-- For every line -->
<DataTemplate>
<TextBlock Name="DialogMessageText"
Text="{Binding NotifyOnTargetUpdated=True}"
VerticalAlignment="Top"
Margin="0,2,0,2"
TextTrimming="WordEllipsis"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
and the code in the codebehind class is like this:
private void DialogItemsControl_TargetUpdated(object sender, System.Windows.Data.DataTransferEventArgs e)
{
ItemsControl itemControl = sender as ItemsControl;
ContentPresenter dp = itemControl.ItemContainerGenerator.ContainerFromItem(itemControl.Items.CurrentItem) as ContentPresenter;
// Finding textBlock from the DataTemplate that is set on that ContentPresenter
DataTemplate myDataTemplate = dp.ContentTemplate;
ItemsControl itc = (ItemsControl)myDataTemplate.FindName("SubDialogItemsControl", dp);
if (itc != null && itc.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
{
ContentPresenter cp = itc.ItemContainerGenerator.ContainerFromIndex(0) as ContentPresenter;
DataTemplate dt = cp.ContentTemplate;
TextBlock tb = dt.LoadContent() as TextBlock;
tb.TargetUpdated += new EventHandler<System.Windows.Data.DataTransferEventArgs>(myTextBlock_TargetUpdated);
}
}
void myTextBlock_TargetUpdated(object sender, System.Windows.Data.DataTransferEventArgs e)
{
TextBlock tb = sender as TextBlock;
//When i access the text property of tb, its showing null, how to get the text
}
When i access the text property of textblock in the target updated event of textblock, its showing null, how to read the text.
Thanks in advance
You tackle the problem from the wrong angle (and probably add a memory leak in the process since I don't see you unsubscribing to the event).
You need to create a Custom TextBlock, overriding the metadata of the Text property so that it changes the Background for a few seconds when the text string changes (through PropertyChangedCallback).
And then use that custom TextBlock in the DataTemplate of your ItemsControl.
EDIT - I thought other people could need this feature so here is a working example:
public class CustomTextBlock : TextBlock
{
static CustomTextBlock()
{
TextProperty.OverrideMetadata(typeof(CustomTextBlock), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(
(dpo, dpce) =>
{
//Flash the background to yellow for 2 seconds
var myTxtblk = dpo as CustomTextBlock;
if (myTxtblk != null)
{
myTxtblk.Background = Brushes.Yellow;
Task.Factory.StartNew(
() =>
{
Thread.Sleep(2000);
Application.Current.Dispatcher.Invoke(
new Action(() =>
{
myTxtblk.Background = Brushes.Transparent;
}));
});
}
})));
}
}
Then you need to declare the right xmlns namespace in your XAML view, and you use it like a regular TextBlock:
<local:CustomTextBlock Text="{Binding MyDynamicText}"/>
It will flash yellow when MyDynamicText is modified (provided it raises PropertyChanged).

Update text in adorner on button click

I have created my custom adorner to cover my main window with a gray canvas alongwith a textblock at center to show some status text while i was working on other window.
What i am currently doing is fetching the required adornerElement(ie Canvas with a textblock) from my resources and passing it to an adorner in my view constructor like this -
ResourceDictionary reportResourceDictionary = App.LoadComponent(new Uri("Resources/ReportResources.xaml", UriKind.Relative)) as ResourceDictionary;
UIElement adornerElement = reportResourceDictionary["RefreshingReportAdorner"] as UIElement;
mainWindowBlockMessageAdorner = new MainWindowBlockMessageAdorner(mainPanel, adornerElement);
But i want to update that text in textblock in some scenarios say if i click on some button in other window but how to update the text dynamically??
Adorner element from Resource file-
<Grid x:Key="RefreshingReportAdorner">
<Rectangle Fill="Gray"
StrokeThickness="1"
Stroke="Gray"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<Border BorderBrush="Black"
BorderThickness="2"
Background="White"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock i18n:LanguageManager.VisualId="6"
Text="Some Text(Update dynamically)"
Padding="15,10,15,10"/>
</Border>
</Grid>
Let me know if additional code or approach required..
Have you tried to create some model and push it to RefreshingReportAdorner element's DataContext?
Code:
var reportResourceDictionary = App.LoadComponent(new Uri("Resources/ReportResources.xaml", UriKind.Relative)) as ResourceDictionary;
var adornerElement = reportResourceDictionary["RefreshingReportAdorner"] as FrameworkElement;
var model = new Model();
model.MyText = "Initial text";
adornerElement.DataContext = model;
mainWindowBlockMessageAdorner = new MainWindowBlockMessageAdorner(mainPanel, adornerElement);
...
model.MyText = "Text after click";
XAML:
<TextBlock i18n:LanguageManager.VisualId="6"
Text="{Binding MyText}"
Padding="15,10,15,10"/>
Model:
public class Item : INotifyPropertyChanged
{
private string _myText;
public string MyText
{
get
{
return this._myText;
}
set
{
this._myText= value;
this.OnPropertyChanged("MyText");
}
}
}

WPF: Getting TreeViewItem's constituent controls

how can I get the constituent controls making up the TreeViewItem in code if they are inside a hierarichicaldatatemplate?
<HierarchicalDataTemplate DataType="{x:Type local:Module}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="Images\module.ico" />
<local:RenamingNode Name="RenamableNode" Text="{Binding Name}" VstaObject="{Binding BindsDirectlyToSource=True}" OnRename="OnRenameOccured" />
</StackPanel>
</HierarchicalDataTemplate>
So programatically when I get an event with a TreeViewItem as the source, I want to be able to get the local:RenamingNode, but I can't get the TreeViewItem's descendants.
Thanks,
Ilya
This worked for me. Doubtless there is a better way, as always, and you will doubtless add extra checks such as checking the Child(ren) count and/or getting/checking type/name of children in a loop etc. but the basic technique works, well it did in my app although I have a Grid instead of a StackPanel.
private object FindContentTemplatePart(TreeViewItem treeViewItem)
{
if (treeViewItem != null)
{
var header = (ContentPresenter)treeViewItem.Template.FindName("PART_Header", treeViewItem);
if (header != null)
{
StackPanel stackPanel = (StackPanel)VisualTreeHelper.GetChild(header,0);
return stackPanel.Children[2];
}
}
return null;
}
You can use FrameworkTemplate.FindName to find the header content presenter in the TreeView item control template, and then again to find the part you want in the data template.
private object FindContentTemplatePart(TreeViewItem treeViewItem)
{
if (treeViewItem != null)
{
var header = treeViewItem.Template.FindName("PART_Header", treeViewItem) as ContentPresenter;
if (header != null)
{
return header.ContentTemplate.FindName("RenamableNode", header);
}
}
return null;
}
You could also walk the visual tree manually with the methods on VisualTreeHelper.
I'm assuming that this will be the same in WPF as silverlight (this is the silverlight version)
(treeViewItem.HeaderTemplate.LoadContent() as StackPanel).FindName("RenamableNode")
None of the above Solutions works in Silverlight
but this seems to work.
<common:HierarchicalDataTemplate x:Key="myHierarchicalTemplate" ItemsSource="{Binding Children}" >
<StackPanel x:Name="spTreeItem" Height="23" Margin="0,0,0,0" HorizontalAlignment="Stretch" Orientation="Horizontal">
</StackPanel>
</common:HierarchicalDataTemplate>
Following the code
TreeViewItem item = TreeViewWorkarounds.ContainerFromItem(trtFolders, trtFolders.SelectedItem);
Grid ItemGrid = (Grid) VisualTreeHelper.GetChild(item, 0);
Button ItemGridButton = (Button)VisualTreeHelper.GetChild(ItemGrid, 2);
Grid ButtonGrid = (Grid)VisualTreeHelper.GetChild(ItemGridButton, 0);
ContentPresenter CP = (ContentPresenter)VisualTreeHelper.GetChild(ButtonGrid, 1);
ContentPresenter CPchlild = (ContentPresenter)VisualTreeHelper.GetChild(CP, 0);
StackPanel sp = (StackPanel)VisualTreeHelper.GetChild(CPchlild, 0);

WPF GridSplitter visiblity

I have a problem regarding GridSplitter visiblity.
In this, whatever I am hosting a Winform DataGridView. The GridSplitter, when dragged is properly visible on other controls. But not on this grid. In fact, whatever I host instead of Datagridview, becomes the topmost control, which makes the GridSplitter hide behind it.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Name="rowForButton"/>
<RowDefinition Name="rowForGridSplitter" Height="Auto" MinHeight="81" />
</Grid.RowDefinitions>
<Button Grid.Row="0" Height="50" Width="110" Content="Button in First Row"/>
<my:WindowsFormsHost Panel.ZIndex="0" Grid.Row="1" Margin="30,11,138,0" x:Name="winHost" Height="58" VerticalAlignment="Top" OpacityMask="Transparent">
<win:DataGridView x:Name="dataGridView"></win:DataGridView>
</my:WindowsFormsHost>
<GridSplitter BorderThickness="1" Panel.ZIndex="1" Grid.Row="1" HorizontalAlignment="Stretch" Height="5" ShowsPreview="True" VerticalAlignment="Top">
</GridSplitter>
</Grid>
Usually you should either put a GridSplitter into its own grid cell or ensure via margins that no control can overlap it. But I don't know whether that exactly applies to you here. See also here.
I encountered this problem also, there is my solution:
var splitter = new GridSplitter()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
FocusVisualStyle = null,
ShowsPreview = true,
Background = new SolidColorBrush(new Color() { R = 1, G = 1, B = 1, A = 1 }),
};
// non-style / essential window which will display over your WinForm control
var PopupWindowForSplitter = new PopupWindow()
{
Background = new SolidColorBrush(new Color() { R = 1, G = 1, B = 1, A = 1 }),
Visibility = Visibility.Collapsed
};
PopupWindowForSplitter.Show();
...
Point _ptForSplitterDrag = new Point(0,0);
splitter.DragStarted += (o, e) =>
{
var pt = splitter.PointToScreen(new Point());
_ptForSplitterDrag = splitter.PointToScreen(Mouse.GetPosition(splitter));
PopupWindowForSplitter.Left = pt.X;
PopupWindowForSplitter.Top = pt.Y;
PopupWindowForSplitter.Height = splitter.ActualHeight;
PopupWindowForSplitter.Width = splitter.ActualWidth;
PopupWindowForSplitter.Activate();
PopupWindowForSplitter.Visibility = Visibility.Visible;
};
splitter.DragDelta += (o, e) =>
{
var pt = splitter.PointToScreen(Mouse.GetPosition(splitter)) - _ptForSplitterDrag
+ splitter.PointToScreen(new Point());
if (splitter.ResizeDirection == GridResizeDirection.Rows)
{
PopupWindowForSplitter.Top = pt.Y;
}
else
{
PopupWindowForSplitter.Left = pt.X;
}
};
splitter.DragCompleted += (o, e) =>
{
var initializeData = typeof(GridSplitter).GetMethod("InitializeData", BindingFlags.NonPublic | BindingFlags.Instance);
var moveSplitter = typeof(GridSplitter).GetMethod("MoveSplitter", BindingFlags.NonPublic | BindingFlags.Instance);
if (moveSplitter != null && initializeData != null)
{
initializeData.Invoke(splitter, new object[] { true });
var pt = splitter.PointToScreen(Mouse.GetPosition(splitter)) - _ptForSplitterDrag;
if (splitter.ResizeDirection == GridResizeDirection.Rows)
{
moveSplitter.Invoke(splitter, new object[] { 0, pt.Y });
}
else
{
moveSplitter.Invoke(splitter, new object[] { pt.X, 0 });
}
}
PopupWindowForSplitter.Visibility = Visibility.Collapsed;
};
Maybe there are some issues in my description because of my poor english, but I think the code is enough to explain it.
Windows Forms controls are always rendered seperately from your WPF controls, and as a result will always appear over your WPF application.
See Hosting a Microsoft Win32 Window in WPF (subheading Notable Differences in Output Behavior) for more info.
Try using a WPF-native DataGrid control. There are a couple of commercial third-party controls you can buy, or you could take a look at one provided by Microsoft (currently still in CTP):
Xceed DataGrid
Telerik RadGridView
Microsoft DataGrid CTP
In your situation, the quickest fix would be to move the GirdSplitter to the Row with the Button:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Name="rowForButton"/>
<RowDefinition Name="rowForGridSplitter" Height="Auto" MinHeight="81" />
</Grid.RowDefinitions>
<Button Grid.Row="0" Height="50" Width="110" Content="Button in First Row"/>
<my:WindowsFormsHost Panel.ZIndex="0" Grid.Row="1" Margin="30,11,138,0" x:Name="winHost" Height="58" VerticalAlignment="Top" OpacityMask="Transparent">
<win:DataGridView x:Name="dataGridView"></win:DataGridView>
</my:WindowsFormsHost>
<GridSplitter BorderThickness="1" Panel.ZIndex="1" Grid.Row="0" HorizontalAlignment="Stretch" Height="5" ShowsPreview="True" VerticalAlignment="Bottom">
</GridSplitter>
</Grid>
Now just adjust the margins to make sure there is some space between the button and the grid splitter.
The solution would be to add a 'Windows Form' label inside the grid splitter, and to do so programmatically after the addition of the DataGridView so it appears on top of it, as follows:
void AddLabelToSplitter()
{
string template =
#" <ControlTemplate
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:mn='clr-namespace:MyNameSpace;assembly=MyAssembly'
xmlns:wf='clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' TargetType='{x:Type GridSplitter}'>
<mn:ExtendedWindowsFormsHost x:Name='Grid_Splitter_WindowsFormsHost' HorizontalAlignment='Stretch' VerticalAlignment='Stretch'>
<wf:Label Dock='Fill' BackColor='DarkGray'></wf:Label>
</mn:ExtendedWindowsFormsHost>
</ControlTemplate>";
Grid_Splitter.Template = (ControlTemplate)XamlReader.Parse(template);
}
Using a regular windows form host would not work, as it wouldn't pass down the mouse events to the splitter, so use the ExtendedWindowsFormsHost instead from below link:
Keep Mouse Events bubbling from WindowsFormsHost on

Resources