i have a textblock that displays multiline messages that are received continuously from the network along with the time it was received. here is the code:
private async Task ReadMessage(TcpClient client, bool ownsClient)
{
using NetworkStream stream = client.GetStream();
byte[] buffer = new byte[4096];
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
DateTime now = DateTime.Now;
string receptiontime = now.ToString("HH:mm");
Dispatcher.Invoke(new Action(() =>
{
NotamsTextBlock.Text += "-->" + receptiontime + Environment.NewLine;
NotamsTextBlock.Text += message;
}), DispatcherPriority.Background);
}
Here is how it is displayed in the app: enter image description here
By default the new message received is inserted in the textblock after the old message. What i want to do is change it. The new message should be inserted from the top which means that when reading the content of the textblock you always start with the newest message.
Any idea on how i can achieve that?
Thanks.
Ps: i'm not using MVVM
Don't use a TextBlock
for this task. The TextBlock
is optimized to display only a few lines at max. It is not designed to display multiline documents.
The recommended solution is to use a ListBox
. It offers virtualization to provide a smooth scrolling experience.
You can override the default template of the ListBoxItem
to remove the mouse over and highlight effects. This way the ListBox
will look like a plain text document or a common console output.
Don't use the Dispatcher
in this case too. If you need to update the ObservableCollection
, which is the data source for the ListBox
, from a background thread, use the BindingOperations.EnableCollectionSynchronization
.
MessageLine.cs
public class MessageLine : INotifyPropertyChanged
{
public MessageLine(string message) => this.Message = message;
public string Message { get; }
public event PropertyChangedEventHandler PropertyChanged;}
public override string ToString()
=> $"Formatted message content: {this.Message}.";
}
MainWindow.xaml.cs
partial class MainWindow : Window
{
public ObservableStack<MessageLine> Messages { get; }
publiv MainWindow()
{
InitializeComponent();
this.Messages = new ObservableStack<MessageLine>();
}
private void InsertLineAtBeginning(MessageLine message) => this.Messages.Push(message);
// Not supported by ObservableStack
//private void AppendLine(MessageLine message) => this.Messages.Add(message);
private async Task ReadMessageAsync(TcpClient client, bool ownsClient)
{
await using NetworkStream stream = client.GetStream();
...
// Since we insert at the beginning but want to have a proper line order of the multi-line message,
// we must insert the last line first (reverse order - you can use a Stack to collect the lines)
string messageBody = Encoding.UTF8.GetString(buffer, 0, bytesRead);
InsertLineAtBeginning(new MessageLine(messageBody));
var messageHeader = $"-->{receptiontime}";
InsertLineAtBeginning(new MessageLine(messageHeader));
}
private async void SaveMessages_OnClick(object sender, EventArgs e)
{
await using var fileWriter = new StreamWriter("destination_file.txt", false);
foreach (var message in this.Messages)
{
// Call ToString() for a defined string representation of the instance.
// Requires to override ToString to get a useful value.
await fileWriter.WriteLineAsync(message.ToString());
// Alternatively, format message inline
await fileWriter.WriteLineAsync($"Formatted message content: {message.Message}");
}
}
}
MainWindow.xaml
<Window>
<ListBox ItemsSource="{Binding RlativeSource={RelativeSource AncestorType Window}, Path=Messages}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:MessageLine}">
<TextBlock Text="{Binding Message}" />
</DataTemplate>
</ListBox.ItemTemplate>
<!-- Remove interaction effects -->
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<ContenPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Window>
ObservableStack.cs
public class ObservableStack<TItem> : Stack<TItem>, INotifyCollectionChanged, INotifyPropertyChanged
{
public ObservableStack()
{
}
public ObservableStack(IEnumerable<TItem> collection) : base(collection)
{
}
public ObservableStack(int capacity) : base(capacity)
{
}
new public void Push(TItem item)
{
base.Push(item);
OnCountPropertyChanged();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, 0));
}
new public bool TryPop(out TItem item)
{
if (base.TryPop(out item))
{
OnCountPropertyChanged();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, 0));
return true;
}
return false;
}
new public void Pop()
{
TItem removedItem = base.Pop();
OnCountPropertyChanged();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItem, 0));
}
new public void Clear()
{
base.Clear();
OnCountPropertyChanged();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) => this.CollectionChanged?.Invoke(this, e);
private void OnCountPropertyChanged() => OnPropertyChanged(propertyName: nameof(this.Count));
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public event PropertyChangedEventHandler? PropertyChanged;
}
You can also move the ListBox
to a UserControl
or custom Control
.