WPF - Getting rid of usage of dispatcher in RichTextBox - c#

I have a WPF application that receives messages from a server.
I listen for messages in a background worker and if a new messages arrives I call a new event.
In my ServerMessage.xaml I had a TextBox that was bound to a property in my ViewModel.
<TextBox IsReadOnly="True" VerticalScrollBarVisibility="Visible" Text="{Binding ServerMessages}" />
In the code-behind of my ServerMessage.xaml.cs I listen for somebody to call the server-message Event.
public void serverMessage(object sender, ServerMessageEventArgs e)
{
viewModel.ServerMessages += e.ServerMessage + "\r\n";
}
Due to certain circumstances I have to switch to a RichTextBox (mostly because of different text-color and images).
Sadly I can't bind anything to the RichTextBox. I tried some of the available Extensions for binding but they didn't work.
To get it working I used the RichTextBox.Dispatcher in the event. It looks like this:
RichTextBox.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.Normal,
new Action(() =>
{
TextPointer caretPos = ChatTextBox.CaretPosition.DocumentEnd;
new InlineUIContainer(new TextBlock() { Text = e.ServerMessage }, caretPos);
RichTextBox.AppendText("\u2028");
RichTextBox.ScrollToEnd();
}
));
Is there a possibility to get rid of the Dispatcher and get back to binding?
Do I gain something if I use binding or is the Dispatcher okay, too?

Related

How to autoscroll AvaloniaUI ScrollViewer when using DataBinding?

I have this setup in my AvaloniaUI application:
<ScrollViewer VerticalScrollBarVisibility="Auto"
AllowAutoHide="True"
Name="MessageLogScrollViewer">
<TextBlock HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
TextWrapping="NoWrap"
Text="{Binding ReceivedMessages}"></TextBlock>
</ScrollViewer>
The TextBlock basically displays log messages and I would like it to autoscroll to the bottom, when I append new lines to the String bound to the text property.
The ScrollViewer has a method called ScrollToEnd(), that I would need to call whenever I update the Text. So I tried to define a function in my code behind like so:
private ScrollViewer messageLogScrollViewer;
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel(this);
messageLogScrollViewer = this.FindControl<ScrollViewer>("MessageLogScrollViewer");
}
...
public void ScrollTextToEnd()
{
messageLogScrollViewer.ScrollToEnd();
}
I then tried to call that function from my ViewModel:
private string receivedMessages = string.Empty;
public string ReceivedMessages
{
get => receivedMessages;
set => this.RaiseAndSetIfChanged(ref receivedMessages, value);
}
...
private MainWindow _window;
public MainWindowViewModel(MainWindow window)
{
_window = window;
}
...
ReceivedMessage += "\n";
ReceivedMessages += ReceivedMessage;
_window.ScrollTextToEnd(); // Does not work.
But unfortunately, this the ScrollToEnd() function needs to be called from the UI-Thread, because I get an Exception:
System.InvalidOperationException: 'Call from invalid thread'
My question is how can I autoscroll my ScrollViewer to the end, whenever I update the TextBlocks Text property via DataBinding?
Ok, I figured it out, but leaving this here for reference.
In order to execute the function from another thread on the UI-Thread one needs to call
Dispatcher.UIThread.InvokeAsync(_window.ScrollTextToEnd);
But this scrolled only to the penultimate line, so it seems, the control is not updated yet, when I call ScrollTextToEnd(). Therefore I added a short timeout in between updating the text and calling the scroll function like this
ReceivedMessages += ReceivedMessage;
// A short timeout is needed, so that the scroll viewer will scroll to the new end of its content.
Thread.Sleep(10);
Dispatcher.UIThread.InvokeAsync(_window.ScrollTextToEnd);
It works now just like that.

WPF: How to have KeyBinding on Window that doesn't get swallowed by textbox

I want to rebind the "Up" key for my window to perform a command on my ViewModel.
My window contains 2 controls: ListView, TextBox.
If I do
<Window.InputBindings>
<KeyBinding Key="F5" Command={Binding SomeCommand} />
</Window.InputBindings>
Everything works correctly. However, if I set it to "Up", or certain other keys, the command does not get executed if the TextBox has focus. This tells me that the TextBox is handling these keys and swallowing the event that triggers the command. Is there any way to prevent that and to allow the window to get them similar to the Preview events (PreviewKeyUp) ?
If you want an application level shortcut, you can use InputManager
InputManager.Current.PreProcessInput += (sender,args)
{
var keyboardEvent = args?.StagingItem?.Input as KeyboardEventArgs;
if(keyboardEvent != null)
{
var keyArgs = keyboardEvent.RoutedEvent as KeyEventArgs;
if(keyArgs != null)
{
if(keyArgs.Key == Key.F5)
{
// Do something
}
}
}
};
I would recommend taking the logic above and incorporating it into a behavior that you can bind/map Keys to Commands.
Is there any way to prevent that and to allow the window to get them similar to the Preview events (PreviewKeyUp) ?
You could hook up an event handler to the KeyDownEvent using the overload of the AddHandler method that accepts a "handledEventsToo" boolean parameter and invoke your command from this event handler:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new ViewModel();
DataContext = vm;
AddHandler(KeyDownEvent, new KeyEventHandler((ss, ee) =>
{
vm.SomeCommand.Execute(null);
}), true);
}
}
There is no way to do this in pure XAML though but it doesn't really matter as far as the MVVM pattern is concerned as you simply move the invocation of the command from the XAML markup of the view to the code-behind class of the very same view. This doesn't break the MVVM pattern.

Windows 10 UWP - GridView Items With no DataContext when Not Visible

I have an issue with a GridView in a UWP application that I'm working on...
Items in the GridView load correctly, however items that are out of view (off the page and not visible) do not have a DataContext assigned, and no event ever fires when the DataContext is assigned. Various bindings do work as TextBlocks that are bound get updated, but the the normal event workflow and Loaded events get all strange.
<GridView Grid.Row="1" Name="SearchGrid" ItemsSource="{Binding SearchItems}" ItemClick="SearchGrid_ItemClick">
<GridView.ItemTemplate>
<DataTemplate>
<local:RsrItemGridViewItem />
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
The grids all show correctly, except, for being able to properly delay load some items because the DataContext isn't set at time of load (and a DataContextChanged event isn't fired when the context is updated).
Does anyone have any ideas how to get notified when the control becomes visible? This seems like a notification bug, or there is some binding thing that I'm missing.
Thank you!
Does anyone have any ideas how to get notified when the control becomes visible?
You can't use FrameworkElement.Loaded event here to get notify when your RsrItemGridViewItem becomes visible, this event occurs when a FrameworkElement has been constructed and added to the object tree, and is ready for interaction.
GirdView control implements UI virtualization for better UI performance, if your GridView is bound to a collection of many items, it might download only items 1-50, When the user scrolls near the end of the list, then items 51 – 100 are downloaded and so on. But for example, there are only 20 items now be shown, but it might have loaded 45 items, 25 items could not be seen in this moment.
If you change the default ItemsPanel of GridView which is ItemsWrapGrid to for example VariableSizedWrapGrid, GridView will lose virtualization, and all items will be loaded at the same time even most of them can not be seen at one moment.
For you problem, I think what you can give a try is calculating the ScrollViewer's VerticalOffset with your GridView's height and the items's count be shown, and then you can know which items are been shown at this moment.
For example here:
private ObservableCollection<MyList> list = new ObservableCollection<MyList>();
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_Loaded;
}
private double viewheight;
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var scrollViewer = FindChildOfType<ScrollViewer>(gridView);
scrollViewer.ViewChanged += ScrollViewer_ViewChanged;
viewheight = gridView.ActualHeight;
}
private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var scrollViewer = sender as ScrollViewer;
var Y = scrollViewer.VerticalOffset;
//calculate here to get the displayed items.
}
public static T FindChildOfType<T>(DependencyObject root) where T : class
{
var queue = new Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
DependencyObject current = queue.Dequeue();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(current); i++)
{
var child = VisualTreeHelper.GetChild(current, i);
var typedChild = child as T;
if (typedChild != null)
{
return typedChild;
}
queue.Enqueue(child);
}
}
return null;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
list.Clear();
for (int i = 0; i < 200; i++)
{
list.Add(new MyList { text = "Item " + i });
}
}
Since GridView control's layout is adaptive to the app's size, the current displayed count is dynamic, you can try other height based properties (for example each item's height) and the ScrollViewer's VerticalOffset to calculate, there is no ready-made method to get your work done, it's a little complex to calculate, but I think there is no better solution for now.
After doing some testing with this, what I found out worked (though it's not very clean, and I believe there is a bug with bindings) was to add the custom control to the GridView, then in the grid view adding a DataContext={Binding} to the Image I wanted to get notified of an update on.
<UserControl ...><Image DataContext="{Binding}" DataContextChanged="ItemImage_DataContextChanged" /></UserControl>
The main control doesn't get notified of a DataContext change, but the child elements are notified.

Can I respond to a Flyout flying out and then assign values to its controls programatically?

I know the canonical way to assign data to controls in the XAML world is to use binding like so in the XAML file:
<ListBox x:Name="lstbxPhotosets" ItemsSource="{Binding photosets}" . . .
...but I would just as soon assign it in code, something like this:
private void flyout_FlewOpen(object sender, RoutedEventArgs reargs)
{
sender.lstbxPhotosets.Items = GetPhotosets();
}
internal static List<String> GetPhotosets()
{
List<String> psets = new List<string>();
using (var db = new SQLite.SQLiteConnection(App.DBPath))
{
string sql = "SELECT photosetName FROM PhotraxBaseData ORDER BY photosetName";
psets = db.Query("sql"); // <= pseudocode; this doesn't compile
}
return psets;
}
Is this possible? If so, what event does the Flyout expose that I can tap into (no pun intended)?
Can I access controls on the Flyout via "sender", or...???
Note: This is a Windows 8.1 app, and a native (not Callista) flyout.
It sounds like you're looking for the Flyout.Opening or Opened event. The Flyout should be the sender, and you can use FrameworkElement.FindName or VisualTreeHelper on the Flyout's Content to find child controls of the Flyout. Data binding will probably be cleaner and easier.
--Rob

WPF problems refreshing textblock bound to console.stdout

I am building a small wpf app in C#. When a button gets clicked a third
party dll function constructs a tree like object. This object is bound
to a treeview. This works fine but takes a bit of time to load. As the
dll function constructs the object it prints progress info to the
console. I want to redirect this into a TextBlock so that the user
gets to see the progress messages.
My window ctor looks like this:
InitializeComponent();
StringRedir s = new StringRedir(ref ProgressTextBlock);
Console.SetOut(s);
Console.SetError(s);
this.DataContext = s;
xaml:
<TextBlock Text="{Binding Path=Text}" Width="244"
x:Name="ProgressTextBlock" TextWrapping="Wrap" />
<TreeView >...</TreeView>
The StringRedir class is shown below. The problem is the TextBlock for
some reason does not get updated with the messages until the TreeView
gets loaded. Stepping through I see the Text property being updated
but the TextBlock is not getting refreshed. I added a MessageBox.Show
() at the point where Text gets updated and this seems to cause the
window to refresh each time and I am able to see each message. So I
guess I need some way to explicitly refresh the screen...but this
doesnt make sense I thought the databinding would cause a visual
refresh when the property changed. What am I missing here? How do I
get it to refresh? Any advice is appreciated!
public class StringRedir : StringWriter , INotifyPropertyChanged
{
private string text;
private TextBlock local;
public string Text {
get{ return text;}
set{
text = text + value;
OnPropertyChanged("Text");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public StringRedir(ref TextBlock t)
{
local = t;
Text = "";
}
public override void WriteLine(string x)
{
Text = x +"\n";
//MessageBox.Show("hello");
}
}
You haven't included the code that is loading the data for the TreeView, but I'm guessing it's being done on the UI thread. If so, this will block any UI updates (including changes to the TextBlock) until it has completed.
So after doing some reading on the WPF threading model ( http://msdn.microsoft.com/en-us/library/ms741870.aspx ) I finally got it to refresh by calling Dispatcher Invoke() with Dispatch priority set to Render. As Kent suggested above UI updates in the dispatcher queue were probably low priority. I ended up doing something like this.
XAML
<TextBox VerticalScrollBarVisibility="Auto"
Text="{Binding Path=Text, NotifyOnTargetUpdated=True}"
x:Name="test" TextWrapping="Wrap" AcceptsReturn="True"
TargetUpdated="test_TargetUpdated"/>
C# target updated handler code
private void test_TargetUpdated(object sender, DataTransferEventArgs e)
{
TextBox t = sender as TextBox;
t.ScrollToEnd();
t.Dispatcher.Invoke(new EmptyDelegate(() => { }), System.Windows.Threading.DispatcherPriority.Render);
}
Note: Earlier I was using a TextBlock but I changed to a TextBox as it comes with scrolling
I still feel uneasy about the whole flow though. Is there a better way to do this?
Thanks to Matt and Kent for their comments. If I had points would mark their answers as helpful.
I believe the problem is in the constructor of your StringRedir class. You're passing in ProgessTextBlock, and you're doing this to it:
local.Text = "";
This is effectively overwriting the previously set value for ProgressTextBlock.Text, which was this:
{Binding Text}
See what I mean? By explicitly setting a value to the TextBlock's Text property, you've cancelled the binding.
If I'm reading right, it looks like the idea of passing a TextBlock into the StringRedir's ctor is a hangover from before you tried binding directly. I'd ditch that and stick with the binding idea as it's more in the "spirit" of WPF.

Categories

Resources