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.
Related
Pulling my hair out at the point. My Picker is showing an annoying line on the UI and/or the Picker's Title property if that's enabled. I simply want the Picker, not the stuff showing on the UI beneath it. Any idea on how to achieve this? Do I have to use a custom renderer or is there something simple I'm missing?
Note: The list is intentionally empty in the below examples.
Without the title, I click the Existing button, the line shows, click it again and the modal appears:
With the title, I click the Existing button, the line and title show, click it again and the modal appears:
Don't know why I have to click the button twice. But it's only on the initial page load. If I exit the modal and click the button again, it immediately appears, no double-click. Not sure if that's related to my original question, but thought I'd include it for additional information.
NewSubjectPage.xaml (chopped for brevity)
<ContentPage.Content>
<StackLayout x:Name="NewSubjectMainLay">
<ScrollView>
<StackLayout x:Name="NewSubjectChildLay">
<Grid>
<Button
x:Name="NewSubjectExisChrtBtn"
Clicked="NewSubjectExisChrtBtn_Clicked"
Grid.Column="2"
Text="Existing" />
</Grid>
</StackLayout>
</ScrollView>
<Picker
x:Name="NewSubjectExisChrtPck"
IsVisible="False"
ItemsSource="{Binding Charts}"
ItemDisplayBinding="{Binding Name}"
SelectedIndexChanged="NewSubjectExisChrtPck_SelectedIndexChanged"
Title="Select chart"
Unfocused="NewSubjectExisChrtPck_Unfocused"/>
</StackLayout>
</ContentPage.Content>
NewSubjectPage.xaml.cs (chopped for brevity)
public partial class NewSubjectPage : ContentPage
{
private string chartName;
private readonly NewSubjectViewModel _viewModel;
public string ChartName
{
get => chartName;
private set
{
chartName = value;
OnPropertyChanged();
}
}
public NewSubjectPage()
{
InitializeComponent();
BindingContext = _viewModel = new NewSubjectViewModel();
chartName = "";
}
private void NewSubjectExisChrtBtn_Clicked(object sender, EventArgs e)
{
_viewModel.LoadChartsCommand.Execute(null);
NewSubjectExisChrtPck.IsVisible = true;
NewSubjectExisChrtPck.Focus();
}
private void NewSubjectExisChrtPck_SelectedIndexChanged(object sender, EventArgs e)
{
var picker = (Picker)sender;
int selectedIndex = picker.SelectedIndex;
if (selectedIndex != -1)
{
ChartName = picker.Items[picker.SelectedIndex];
}
}
private void NewSubjectExisChrtPck_Unfocused(object sender, FocusEventArgs e)
{
NewSubjectExisChrtPck.IsVisible = false;
NewSubjectExisChrtPck.Unfocus();
}
}
NewSubjectViewModel.cs (chopped for brevity)
class NewSubjectViewModel : BaseViewModel
{
private ObservableCollection<Chart> charts;
public ObservableCollection<Chart> Charts
{
get { return charts; }
private set
{
charts = value;
OnPropertyChanged();
}
}
public Command LoadChartsCommand { get; set; }
public NewSubjectViewModel()
{
LoadChartsCommand =
new Command(
async () => await ExecuteLoadChartsCommand()
);
}
private async Task ExecuteLoadChartsCommand()
{
try
{
IndicatorRunning = true;
var list = await App.Database.GetChartsAsync();
Charts = new ObservableCollection<Chart>(list);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
}
Thanks for your help! Let me know if you need to see anything else.
First, I was not able to reproduce the issue of the modal not showing until a second click of the button. You might need to provide more code for that to happen. To even use your code sample I had to replace var list = await App.Database.GetChartsAsync(); with something else to simulate a long running task that returns an empty list. Also had to create a Chart type with a Name property. Not to mention BaseViewModel. In the future, please provide all code to reproduce the issue so there is minimal work required of the person who is trying to help you. There is concept on Stack Overflow called the MCVE (minimal, complete, verifiable example): http://stackoverflow.com/help/mcve
That said, perhaps the first click is actually focusing the emulator and making it the active app, and then the second is the first actual click on the button? This I can reproduce. IOW, if the emulator is not the foreground app, you have to click it once to make it active and then your app will handle clicks.
As for the undesirable UI, you do realize that the Picker UI is basically a clickable label that when clicked displays the actual picker modal? So when you make it visible, what you are making visible is the label UI, which has the line and the Title (if set), and when you focus that label, then the actual picker dialog is displayed. If you don't want to see the UI Label at all, then why make it visible? You can focus it without making it visible, so just remove the line NewSubjectExisChrtPck.IsVisible = true;
As a side note, when you call _viewModel.LoadChartsCommand.Execute(null); that calls an async method, var list = await App.Database.GetChartsAsync(); , so the LoadChartsCommand returns before you set the Charts property, and also then the code following the call to _viewModel.LoadChartsCommand.Execute(null); also executes before LoadChartsCommand really finishes, so you are making the picker visible and focusing it before the LoadChartsCommand finishes as well, so if you were loading actual items for the picker to display, they may not be there the first time. Maybe it's just the sample code, but I see no reason to use a command here, but rather you should just call an awaitable task. You are not binding to the LoadChartsCommand, so I see no reason for you to even use a Command in this scenario. Instead I suggest making ExecuteLoadChartsCommand public and calling it directly, e.g.:
private async void NewSubjectExisChrtBtn_Clicked(object sender, EventArgs e)
{
//_viewModel.LoadChartsCommand.Execute(null); // Returns immediately, so picker not loaded with items yet.
await _viewModel.ExecuteLoadChartsCommand(); // Waits for method to finish before before presenting the picker.
//NewSubjectExisChrtPck.IsVisible = true;
NewSubjectExisChrtPck.Focus();
}
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?
I have a XAML layout similar to this:
<Grid>
<TextBox x:Name="inputTextBox" LostFocus="inputTextBox_LostFocus" TextChanged="inputTextBox_TextChanged" GotFocus="inputTextBox_GotFocus" />
<ComboBox x:Name="inputComboBox" SelectionChanged="inputComboBox_SelectionChanged">
<ComboBoxItem IsSelected="True">10</ComboBoxItem>
<ComboBoxItem>15</ComboBoxItem>
<ComboBoxItem>20</ComboBoxItem>
</ComboBox>
<ComboBox x:Name="inputComboBoxTwo" SelectionChanged="inputComboBoxTwo_SelectionChanged">
<ComboBoxItem IsSelected="True">1</ComboBoxItem>
<ComboBoxItem>2</ComboBoxItem>
<ComboBoxItem>3</ComboBoxItem>
</ComboBox>
</Grid>
Pretty simple. In the codebehind C# file, I use these controls to take in a double from the TextBox, some more ints from the ComboBoxes, then I create a calculator type object with the data from the controls. I make the calculation and display the results in some other TextBlocks.
namespace TipCalc
{
public sealed partial class MainPage : Page
{
Calc x = new Calc();
public MainPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
}
//
//Appropriate event handlers from XAML controls that all call the calculation method.
//
private void calcIt()
{
x.amt = double.Parse(inputTextBox.Text);
x.cal1 = int.Parse(inputComboBox.SelectedItem.ToString());
x.cal2 = int.Parse(inputComboBoxTwo.SelectedItem.ToString());
//Send calculated values to output TextBlocks.
}
}
}
When I run this program, I hit a null pointer exception that is thrown when I attempt to access the text property of the TextBox. It turns out that all of the XAML controls are null. However, _contentLoaded is set to true and the code definition for this.IntializeComponent looks correct behind the scenes.
Why are all my controls set to null when it seems like everything is working correctly? Is there a way to manually initialize them if they aren't correctly being initialized automatically? Am I doing anything wrong?
the code run like:
Calc x = new Calc();
this.InitializeComponent();
Calc() was first than InitializeComponent(), but InitializeComponent() create your controls.
you can change to:
Calc x;
public MainPage()
{
this.InitializeComponent();
x = new Calc();
this.NavigationCacheMode = NavigationCacheMode.Required;
}
I have the same problem with some of my TextBox controls when the class initializes. What I did to solve this is not the real and perfect solution because not all the controls (TextBox, ComboBox, RadioButton, etc) are null when the class is running, and there's something happening in my code or my app or my VS that I'm missing or doing wrong.... But at least is working fine now. I hope is useful to you:
if(TextBox1 == null)
{
//I'm re-initializing the control because is null
TextBox1 = new TextBox();
}
For your code it should be something like this:
if(inputTextBox == null)
{
inputTextBox.Text = new TextBox();
}
x.amt = double.Parse(inputTextBox.Text);
I hope this 'solution' is good enough for you. And for my poor English I apologize if I have mistakes, is not my native language.
I'm currently trying to have a pop-out button, such that when it's clicked the current grid will appear in a new window populated with the exact same information.
I got the new Window to appear but I'm trying to have the bindings set but unsure how to do that. If I can get some help please. When I execute OpenChildWindow it opens but nothing populates.
Viewmodel:
public ObservableCollection<PaymentInfo> AmortizationCollection {get; set;}
public void OpenChildWindow()
{
new ScheduleView().Show();
}
LoanView.xaml and ScheduleView.xaml
<telerik:RadGridView Grid.Row="3" Grid.ColumnSpan="3" x:Name="AmortGrid"
ScrollViewer.CanContentScroll ="True"
SelectedItem="{Binding SelectedRow, Mode=TwoWay}"
Height="650" AutoGenerateColumns="True" ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ShowGroupPanel="False"
ItemsSource="{Binding AmortizationCollection, Mode=TwoWay}">
My attempt at Content Setting
Scheduleview.xaml.cs
public ObservableCollection<PaymentInfo> AmortizationCollection { get; set; }
public ScheduleView()
{
InitializeComponent();
AmortGrid.ItemsSource = Content;
}
Viewmodel:
public void OpenChildWindow()
{
ScheduleView _newScheduleView = new ScheduleView();
_newScheduleView.Content = AmortizationCollection;
_newScheduleView.Show();
}
The window just appears (Collection) no datagrid or anyhting.
When using WPF, we tend to use DataTemplates to define what our data objects look like in the UI, so to recreate some UI control, we just need to pass the data object and ensure that the DataTemplate is accessible from both locations. You show code from your 'view model', but view models shouldn't know anything about the views and certainly don't open new windows like that.
However, incorrect terminology aside, your simplest solution would be to simply pass the collection to the new Window in the constructor:
public void OpenChildWindow()
{
ScheduleView _newScheduleView = new ScheduleView(AmortizationCollection);
_newScheduleView.Show();
}
Then in ScheduleView.xaml.cs:
public ScheduleView(ObservableCollection<PaymentInfo> amortizationCollection)
{
InitializeComponent();
AmortizationCollection = amortizationCollection;
AmortGrid.ItemsSource = AmortizationCollection;
}
As long as you have the same XAML in each place, it should look the same.
If that is a Window then you can try to set some Content to it, this way the new control that you'll pass will be set as the content of the new window. I think this property is missing in your code untill now.
// create instance
ScheduleView wind = new ScheduleView();
// set the content, can be a window or a page or anything
wind.Content = new SomeControl;
// show it.
wind.Show();
http://msdn.microsoft.com/en-us/library/system.windows.window.content(v=vs.95).aspx
A previous thread from Stack Overflow has this, but in a manner that was too general.
Changing content of Window (WPF)
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.