WPF Scroll To End of FlowDocument - c#

I'm having trouble with getting a FlowDocument wrapped in a FlowDocumentScrollViewer to scroll to the end when its data changes.
This is my XAML code
<core:CustomFlowDocumentScrollViewer x:Name="ScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<FlowDocumentScrollViewer.Document>
<FlowDocument PagePadding="0">
<Paragraph Name="Paragraph"></Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer.Document>
</core:CustomFlowDocumentScrollViewer>
The core:CustomFlowDocumentScrollViewer implements the following snippet https://stackoverflow.com/a/561319/13567181, just so I can call ScrollToBottom() later.
In my code-behind I'm clearing the Paragraph and adding new lines to it
private void PopulateFlowDocument(IEnumerable<LoggingEvent> list)
{
Paragraph.Inlines.Clear();
foreach (var loggingEvent in list)
{
var parsedRun = FormatLoggingEvent(loggingEvent);
Paragraph.Inlines.Add(parsedRun);
Paragraph.Inlines.Add(Environment.NewLine);
}
}
Once PopulateFlowDocument completes, I call ScrollToEnd - The control performs some level of scrolling but does not work reliably. My datasource always returns 5000 rows but the scroll view only scrolls to line ~3750 (sometimes more, sometimes less).
Does FlowDocument work asynchronously internally???
Approach 2
Apart from the solutions available on SO, I've also tried the following suggestion from the MSDN forum.
void paragraph_Loaded(object sender, RoutedEventArgs e)
{
Paragraph paragraph = (Paragraph)sender;
paragraph.Loaded -= paragraph_Loaded;
paragraph.BringIntoView();
}
Like above, the Loaded event triggers to early and hence the scrolling does not work reliably.
What I'm looking for is an event/notification when the entire document has been updated so that I can reliably scroll to its very bottom.

I ended up achieving what I wanted using the dispatcher. I borrowed the idea from here
Dispatcher.Invoke(new Action(() =>
{
SearchResultTextBox.ScrollToEnd();
}), DispatcherPriority.ContextIdle, null);
It seems that the FlowDocument is using the dispatcher too and so the scroll needs to be low-priority enough so that its scheduled after the actual document modifications.

Related

GridView doesn't scroll down to virtualized items

In my application, I need to select the newly created document(note) when I go back to library. After library item is selected, the Library must be scrolled to the selected item.
My library's OnLoaded method:
private async void OnLoaded(object sender, RoutedEventArgs e)
{
await this.ViewModel.InitializeAsync();
// CollectionViewSource of my GridView being filled
ViewModel.CollectionChanging = true;
GroupInfoCVS.Source = ViewModel.GroupsCollection;
ViewModel.CollectionChanging = false;
// Loading Last selected item - THIS CHANGES SELECTION
ViewModel.LoadLastSelection();
}
After I call the LoadLastSelection method, selection is changed successfuly (I've tested). This is the method that is called after that (in our GridView's extended control):
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsCount = this.SelectedItems.Count;
var newlySelectedItems = e.AddedItems;
if (newlySelectedItems.Any())
{
var item = newlySelectedItems.Last();
ScrollTo(item);
}
}
private void ScrollTo(object item)
{
UpdateLayout();
var itemUI = (FrameworkElement)this.ContainerFromItem(item);
if (itemUI != null)
{
_scrollViewer.UpdateLayout();
_scrollViewer.ChangeView(null, itemUI.ActualOffset.Y - itemUI.ActualHeight, null, false);
}
}
This also works for the most part. When itemUI is not null, the method scrolls successfully to the required item. The problems start when the items start to overflow the screen size. When items are completely hidden from the screen, they are virtualized. That means that ContainerFromItem returns null, so I can't take the offset properties. Keep in mind that this actually occurs before Library's OnLoaded method is finished.
Please, help me with some alternative to get such properties or other methods of scrolling, which will help me scroll successfully.
I've read a lot and tried using Dispatcher.RunAsync and ScrollIntoView methods, but I couldn't manage to produce any scrolling behavior. If you point me how to use them successfully, that would be a nice help too.
Here's what I've read (and tried):
ItemContainerGenerator.ContainerFromItem() returns null?
How to Know When a FrameworkElement Has Been Totally Rendered?
Is there a "All Children loaded" event in WPF
Let ListView scroll to selected item
Thanks in advance!
IMPORTANT: If you don't want to read all the conversation within the official answer, please read the solution in short here:
TemplatedControl's style had changed ScrollViewer's name from "ScrollViewer" to "LibraryScrollViewer" and that rendered ScrollIntoView method useless.
For GridView, the best way to achieve your needs is to call GridView.ScrollIntoView.
But you seem to have made similar attempts, and it does not to be successful, then the following points may help you:
1. Don't use GridView as a child element of ScrollViewer.
In your code, I see that you are calling the method of ScrollViewer.ChangeView to adjust the view scrolling, so it is speculated that you may put the GridView in the ScrollViewer, which is not recommended.
Because there is a ScrollViewer inside the GridView, and its ScrollIntoView method is to change the scroll area of the internal ScrollViewer. When there is a ScrollViewer outside, the ScrollViewer inside the GridView will lose the scrolling ability, thus making the ScrollIntoView method invalid.
2. Implement the Equals method of the data class.
If your data class is not a simple type (such as String, Int32, etc.), then implementing the Equals method of the data class will help the GridView to find the corresponding item.
Thanks.

Wait for the UI to redraw before "taking a screenshot" of it

The challenge
I'm trying to create a Window that'll draw a card on a page of a PDF document.
The idea is to receive a collection (of the items that should be drawn on the card), then go through them, and for each of the items, load it to a UniformGrid (to give the impression of a actual card), capture an image of the screen and draw it to a page of a PDF document.
So far so good.
What I've tried (and partially succeeded)
Here's what I've tried:
(the following code was added to a Loaded event handler of the window)
PdfDocument document = new PdfDocument();
// This is just an example
int[][] items = new int[10][];
// Fill the matrix
foreach (int[] array in items)
{
Dispatcher.Invoke(() =>
{
// uGrid is an UniformGrid
uGrid.Children.Clear();
foreach (int id in array)
{
Border child = null;
// Create child element and add it to the Uniform Grid
uGrid.Children.Add(child);
}
});
Dispatcher.Invoke(new Action(() =>
{
// Draws an image of 'this' to the PDFDocument 'document'
PdfHelper.DrawPictureOfControlToPdf(document, this);
// DispatcherPriority.ContextIdle here is responsible for executing the DrawPicture method only after the uGrid has been redrawed
}), DispatcherPriority.ContextIdle, null);
}
// This will save the PDF on your documents folder
document.Save(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MyTestPDF.pdf"));
A full (but very short, I promise) working example can be found on my GitHub
Also, the produced PDFs (when debugging and when not - see problem below), can be found here
The problem
The above code works great when on debugging mode.
When debugging: the code runs without errors, the PDF is created with the correct number of pages, and the cards are correctly drawn on it.
When not: the code runs without errors, the PDF is created with the correct number of pages, but the cards are not drawn on it.
I think that what's causing the issue is the DispatcherPriority.ContextIdle on the following piece of code:
Dispatcher.Invoke(new Action(() =>
{
PdfHelper.DrawPictureOfControlToPdf(document, this);
}), DispatcherPriority.ContextIdle, null);
It should indicate that the Action must only be run when the UniformGrid finished redrawing (or after the Dispatcher took care of it, more precisely). But apparently this only works inside debugging mode.
Does someone know how to solve this?
If not, is there any alternative that I'm not thinking of?
#EDIT
I don't think this is important, but I'm using PDFSharp to generate/handle the PDF
Remove all Dispatcher.Invoke in your code (they are not doing anything good here), and then call
this.UpdateLayout();
to force redraw of current control, before calling PdfHelper.DrawPictureOfControlToPdf(document, this);.

Does Grid.Children.Clear() really remove all controls?

Edit:
Simple answer: Yes, it does. I found the error, which was, that another event handler was added, everytime the Combobox_SelectionChanged was fired. Hence, the collection looked fine, but the Items_CollectionChanged was fired multiple times. Once this was fixed, everything worked fine.
End Edit.
I've got a page with a combobox and a grid. The grid fills dynamically, when the selection in the combobox changes. I'm now observing a strange thing. When I select a value for the second time in the combobox, the childitems in the grid appear twice. I've checked the underlying collections, which look fine (i.e. only one record per item). When I jump out of the combobox_SelectionChanged method, after the Grid.Children.Clear() command, the screen looks fine, i.e. empty.
My guess is, that Grid.Children.Clear() only removes the controls from the visual tree, but the actual controls are still hanging around. Any Ideas?
private void combobox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
grItems.Children.Clear();
grItemsColumnDefinitions.Clear();
grItemsColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(200) });
}
private void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
grItems.Children.Add(new ChildItemControl(e.NewItems[0]));
}
}
Edit: The whole thing is supposed to look like this (fictional - but hopefully understandable - example)
I would suggest you use the built in Databinding for WPF. You could use something like this:
<DataGrid x:Name="grItems" ItemsSource="{Binding comboboxItems}" />
Then, when you update comboboxItems your grid will automatically update too!
Here is a great article on Databinding with the DataGrid control:
http://www.wpftutorial.net/DataGrid.html
For more information about Databinding in general, here is a good article: https://msdn.microsoft.com/en-us/library/aa480224.aspx

UI not updating from EventAggregator

For a project I use a very simple progress overlay.
It just displays a small marquee progressbar and covers the screen.
So in my ShellView I have
<Border Grid.Row="0"
Grid.RowSpan="11"
Grid.Column="0"
Grid.ColumnSpan="11"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Panel.ZIndex="3"
Background="#9E000000"
BorderBrush="Black"
BorderThickness="3"
Visibility="{Binding IsProgressing,
Converter={StaticResource BoolToVisibility}}">
<!-- omitted progressbar, text etc -->
</Border>
And I have a very simple event, which just sets the Visibility (IsProgressing) binding and some text to show.
Whenever I want to have a progressbar, I just publish that event, like
_eventAggregator.Publish(new ProgressingChange(true, "Loading ..."));
This works very well so far, besides one case:
For the application I use events for navigation of my screens.
So there is another event which I publish, like:
_eventAggregator.Publish(new NavigationEvent(typeof(TargetViewModel)));
which just sets the target screen:
public void Handle(NavigationEvent navigate)
{
var target = _screenFactory.FromType(navigate.TargetScreen);
this.ActivateItem(target);
}
One of my Screens has lots of items and takes about 3 seconds to load.
So I wanted to show my Progress overlay while the screen is loading.
This is what does not work. Both the new Screen and the Overlay are
showing simultaneously when those events are combined.
This is:
_eventAggregator.Publish(new ProgressingChange(true, "Loading ..."));
_eventAggregator.Publish(new NavigationEvent(typeof(LongLoadingViewModel)));
For debugging reasons I did not deactivate the Progressing overlay to the
what is happening.
So the screen is loaded, nothing is shown on the screen for about 3 seconds, and then
both the Progress overlay and the new screen are shown.
I have tried
sleeping after publishing the ProgressingChange event
sleeping in both event handlers
running the publish of both events on separate threads
running only one of those publishes on a separate threads
tried to force UI update like this here force UI update
What am I missing?
What is happening here?
How can I get that to work?
-edit-
Here is the code of my Handler method:
public void Handle(ProgressingChange progressing)
{
this.IsProgressing= progressing.IsProgressing;
this.ProgressingText= progressing.ProgressingText;
NotifyOfPropertyChange(() => IsProgressing);
NotifyOfPropertyChange(() => ProgressingText);
// of course, there is Notify in the setters themselves, too
}
And I used this code from the source linked above to force UI updates,
but that did not work
void AllowUIToUpdate() {
DispatcherFrame frame = new DispatcherFrame();
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Render, new DispatcherOperationCallback(delegate(object parameter)
{
frame.Continue = false;
return null;
}), null);
Dispatcher.PushFrame(frame);
}
Also, I tried publishing in a critical section to force the first
publish to be executed before the second one, but that did not work either.
-edit2-
Code that at least shows the Progress overlay
_eventAggregator.Publish(new ProgressingChange(true, "Activating ..."));
Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => _eventAggregator.Publish(new NavigationEvent(typeof(LongLoadingViewModel)))), DispatcherPriority.ContextIdle, null);
It might be a hack, but you could try to first wait for the Border (porgress bar) to be rendered before the navigation-event is published. To do this, you might be able to adapt the solution that is given here to execute some code when the UI-thread is no longer busy (see link for full explanation):
Dispatcher.BeginInvoke(new Action(() => Trace.WriteLine("DONE!", "Rendering")), DispatcherPriority.ContextIdle, null);
If this works, then at least you have something to start off making the code a bit cleaner.

Get "is ready" signal after rendering of WPF toolkit chart

I am using the charting stuff of the WPF toolkit.
After creating a chart I like to have a snaphshot of that chart, without visualizing that chart. My problem is that I don't know when the rendering process is done, so I can create a snapshot.
I tried listening to the "LayoutUpdated" event, but the chart is being updated very often.
Can anyone tell me how to find out if the chart is completely rendered?
You can use the Dispatcher in your code behind:
this.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
// code in here should be executed after chart has been rendered.
}
));
The Loaded event sounds like what you need.
MyChart.Loaded += (sender, e) =>
{
// chart has been loaded but not yet rendered
}
where "MyChart" is a the name you've given the chart in your XAML.
I'm not sure how well this works with frequent updates, but this link seems to indicate http://blogs.msdn.com/b/mikehillberg/archive/2006/09/19/loadedvsinitialized.aspx that this is what you're looking for.

Categories