WPF Chatbox control - c#

currently I'm working on a chat client and changed my client from windows forms (as forms is shitty) to WPF. I'm not really sure which control could be used to realize a chatbox. I could take a TextBox but this will not display the complete content when it's full. I also tried to use a ListBox but when I try to add items they are not displayed. I used this code to add content to it:
internal void AddMessage(string message)
{
listBox_messages.Items.Add(message);
listBox_messages.Items.Refresh();
}
Does anybody knows which control would be the best for this purpose?
Thanks for your help!
Edit:
I implemented a TextBox for this and disabled it. But the text I'm appending by this method is not shown.
My class:
using System.ComponentModel;
using System.Windows;
namespace Chat_Client
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
/// <summary>
/// MainWindow constructor
/// </summary>
public MainWindow()
{
InitializeComponent();
textBox_messages.AppendText("Test" + "\n");
textBox_messages.AppendText("Test" + "\n");
textBox_messages.AppendText("Test" + "\n");
Closing += OnWindowClosing;
}
private void OnWindowClosing(object sender, CancelEventArgs e)
{
Program.Shutdown();
}
private void button_connect_Click(object sender, RoutedEventArgs e)
{
if(Program.Connected)
{
Program.Disconnect();
}
else
{
Program.Connect();
}
}
private void button_sendMessage_Click(object sender, RoutedEventArgs e)
{
}
internal void AddMessage(string message)
{
textBox_messages.AppendText(message + "\n");
}
}
}
The test strings are shown but the text added by the method AddMessage is not. I can verify that the method is called, I just checked it with a breakpoint inside this method. Anybody has a clue how this could happen?

Update
If the Program class calls AddMessage(string) from within another thread than the UI thread, you have to use a Dispatcher in order to update the UI.
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
mainWindow.AddMessage(message);
}));
MVVM is the way to go when using WPF.
Add your messages to an ObservableCollection in your ViewModel class (ChatViewModel.cs):
public class ChatViewModel
{
public ObservableCollection<string> Messages { get; } = new ObservableCollection<string>();
internal void AddMessage(string message)
{
Messages.Add(message);
}
}
Set this ViewModel as the DataContext of your View (ChatView.xaml)
public ChatView : UserControl
{
public ChatView()
{
InitializeComponent();
DataContext = new ChatViewModel();
}
}
In the XAML-Code bind the ObservableCollection to the ItemsSource property of your ListBox:
<ListBox ItemsSource="{Binding Messages}") />
When you add a message to the Messages collection it should appear inside the ListBox. This is not a complete example, but it should guide you into the right direction.

*In response to your edit: where are you calling AddMessage() from? I tried your code and just called AddMessage("foo"); from the button click event and worked fine.
For a chat box, I would use a TextBox to write the chat messages, and wrap it with a ScrollViewer for scrolling. After using AppendText() on the TextBox to write the message, you can then call ScrollToEnd() to scroll to the bottom of the TextBox.
In the XAML:
<ScrollViewer x:Name="ScrollViewer" ScrollChanged="ScrollViewer_OnScrollChanged">
<TextBox x:Name="ChatBox"/>
</ScrollViewer>
In the code behind:
private void WriteToChat(string message)
{
ChatBox.AppendText(message);
ChatBox.ScrollToEnd();
}
private bool _autoScroll = true;
private void ScrollViewer_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e.ExtentHeightChange == 0)
{
_autoScroll = ScrollViewer.VerticalOffset == ScrollViewer.ScrollableHeight;
}
if (_autoScroll && e.ExtentHeightChange != 0)
{
ScrollViewer.ScrollToVerticalOffset(ScrollViewer.ExtentHeight);
}
}

You can use a TextBox and set the VerticalAlignment to Stretch in your Xaml file.
< TextBox x:Name="textbox" VerticalAlignment="Stretch">

Related

Add custom event to a CustomControl in WinUI 3

I've created a CustomControl in WinUI 3 with some custom properties (DependencyProperty).
Now I also want to add a custom Event (not sure if it has to be a RoutedEvent?) that has to trigger whenever the CurrentStatus property of my control changes. I'd like to be able to manage it just like a simple Button's Click, so something like:
public sealed class MyCustomTextBlock : Control
{
public enum Status
{
Waiting,
Busy,
}
private Status currentStatus;
public Status CurrentStatus
{
get { return currentStatus; }
}
public MyCustomTextBlock()
{
this.DefaultStyleKey = typeof(MyCustomTextBlock);
currentStatus = Status.Waiting;
}
// The currentStatus variable is set on some custom methods inside the implementation of the control.
}
<local:MyCustomTextBlock x:Name="MessageBlock"
Message="{TemplateBinding Message}"
DisplayMode="{TemplateBinding DisplayMode}"
StatusChanged="MessageBlock_StatusChanged">
</local:MyCustomTextBlock>
private void MessageBlock_StatusChanged(object sender, RoutedEventArgs e)
{
// Check the CurrentStatus property
if (MyCustomTextBlock.CurrentStatus == MyCustomTextBlock.Status.Waiting)
// Do something...
}
How should I declare the Event and the EventHandler on the code behind of the MyCustomTextBlock control?
I'm new to WinUI 3 and XAML and looking for "winui 3 custom event" I found this but it's related to WPF and so I cannot find the EventManager.RegisterRoutedEvent() which is cited in the WinUI Microsoft.UI.Xaml library.
Update 2022-09-13
I followed #AndrewKeepCoding suggestion and now this is my code on the MainWindow page where I placed MyCustomTextBlock control.
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<local:MyCustomTextBlock x:Name="MessageBlock"
Message="{TemplateBinding Message}"
DisplayMode="{TemplateBinding DisplayMode}"
StatusChanged="MessageBlock_StatusChanged">
</local:MyCustomTextBlock>
</StackPanel>
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
private void MessageBlock_StatusChanged(object sender, StatusChangedEventArgs e)
{
MyCustomTextBlock control = (MyCustomTextBlock)sender;
if (e.NewValue == Status.Waiting)
{
// Do something...
}
else if (e.NewValue == Status.Busy)
{
// Do something else...
}
}
}
There's no warnings or compilation errors, but now every time I start the application in debug mode it crashes without trapping any specific Exception with the following error description:
Win32 unmanaged Exception in [10584] WinUI3TestApp.exe
The MainWindow.InitializeComponent() method is called without any issue but after passing the last line of the the public MyCustomTextBlock() constructor method, I don't know what happens but something makes the app crash.
Update 2022-09-14
It seems to be related to this Microsoft.UI.Xaml Issue. There's probably something wrong somewhere inside an async method but it's not possible to catch the Exception due to this bug.
Workaround
My problem is related to the Event handler method added in XAML (StatusChanged="MessageBlock_StatusChanged").
After removing it and replacing with the event handler declaration (MessageBlock.StatusChanged += MessageBlock_StatusChanged;) in code behind on MyCustomTextBlock.Loaded() method, everything worked correctly.
You can implement a custom EventHandler like this.
public class StatusChangedEventArgs : EventArgs
{
public Status OldValue { get; }
public Status NewValue { get; }
public StatusChangedEventArgs(Status oldValue, Status newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
}
public sealed class MyCustomTextBlock : Control
{
public enum Status
{
Waiting,
Busy,
}
private Status currentStatus;
public Status CurrentStatus
{
get { return currentStatus; }
}
public MyCustomTextBlock()
{
this.DefaultStyleKey = typeof(MyCustomTextBlock);
currentStatus = Status.Waiting;
}
public event EventHandler<StatusChangedEventArgs>? StatusChanged;
protected void OnStatusChanged(Status oldValue, Status newValue)
{
StatusChanged?.Invoke(this, new StatusChangedEventArgs(oldValue, newValue));
}
}

How to Edit Text From Another Class In another Class?

i would like change the text of in a class i transfered from another class in an Entry ,this is my code
public class contentPage:ContentPage
{
public Entry TextEnrty;
public Button doneButton;
public contentPage()
{
doneButton.Clicked+=doneButtonClicked;
Content =new StackLayout
{
Children = {TextEntry,doneButton}
}
}
private void doneButton_Clicked(object sender, System.EventArgs e)
{
App.Current.MainPage= new ContentPage2(TextEntry.text)
}
}
public class ContentPage2:ContentPage
{
public label TextLabel;
public Button EditButton;
public ContentPage2(string parameter)
{
EditButton.clicked+=EditButtonClicked;
TextLabel.Text = parameter;
Content =new StackLayout
{
Children = {TextLabel,EditButton}
}
}
private void EditButtonClicked(object sender, System.EventArgs e)
{
App.Current.MainPage= new contentPage()
}
}
According to the code when Text is entered in the TextEntry in ContentPage, The TextEntry.Text Is sent as a parameter to contentpage2 after doneButton is clicked, and the value is set equal to TextLabel.Text,
Now, i want to edit the TextLabel.text,but since i am not using the navigation button bit EditButton, the old text will not show in the TextEntry after i go back to edit the text.
So my problem is that , i want the old text to show when i go back to chenge the text and not an empty Entry.
Why don't you just make a second constructor like this:
contentPage(string entryText)
{
InitializeComponent();
TextEnrty.Text=entryText;
}
And in the line in your secon class where you "navigate" back to your class, when creating a new instance of contentPage use this new Constructor.
you should try MVVM pattern and share viewmodel across this two view
<Application.Resources>
<local:ViewModel x:Key="sharedViewModel" />
</Application.Resources>

Listbox not updating within user control wpf

I've been struggling for a while with getting a listbox to update in WPF. I have a user control that includes a listbox. I then embed this usercontrol within another user control.
public partial class MyClass : UserControl
{
private Thread myThread;
public MyClass()
{
InitializeComponent();
this.DataContext = this;
}
ObservableCollection<String> sampleData = new ObservableCollection<String>();
public ObservableCollection<String> SampleData
{
get
{
if (sampleData.Count <= 0)
{
sampleData.Add("1");
sampleData.Add("2");
}
return sampleData;
}
}
public void StartMyThread()
{
if (myThread == null)
{
myThread = new Thread(ThreadProgram);
myThread.Start();
}
}
private void UpdateFromThread(string text)
{
this.Dispatcher.Invoke((Action)(() =>
{
UpdateList(text);
}));
}
private void UpdateList(string text)
{
sampleData.Add(text);
}
private void ThreadProgram()
{
UpdateFromThread("my text");
//other stuff
}
My XAML for the user control that contains the list box contains:
<Grid>
<ListBox ItemsSource="{Binding Path=SampleData}"/>
</Grid>
and the place where I embed my control is here:
<TabControl SelectionChanged="TabControl_SelectionChanged" Name="tabCtrl">
<TabItem Header="MyListboxTab">
<Service:MyClass Margin="0,0,-1,0"/>
</TabItem>
It updates initially with the "1" and "2" and I can use a messagebox to display the text instead. I've checked and my updates to the listbox are definitely being called, they just don't display.
It's also worth noting that the list updates when calling sampleData.Add("clicked"); from a button click.
Any help is much appreciated, thanks.

How to bring window to front with wpf and using mvvm

I have a window that essentially runs a timer. When the timer hits 0 I want to bring the window to the front so that it is visible and not hidden behind some other application.
From what I can gather I would simply call window.activate() to accomplish this but with mvvm my view model doesn't have a reference to window.
A "purist" MVVM solution is to use a behavior. Below is a behavior for a Window with an Activated property. Setting the property to true will activate the window (and restore it if it is minimized):
public class ActivateBehavior : Behavior<Window> {
Boolean isActivated;
public static readonly DependencyProperty ActivatedProperty =
DependencyProperty.Register(
"Activated",
typeof(Boolean),
typeof(ActivateBehavior),
new PropertyMetadata(OnActivatedChanged)
);
public Boolean Activated {
get { return (Boolean) GetValue(ActivatedProperty); }
set { SetValue(ActivatedProperty, value); }
}
static void OnActivatedChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) {
var behavior = (ActivateBehavior) dependencyObject;
if (!behavior.Activated || behavior.isActivated)
return;
// The Activated property is set to true but the Activated event (tracked by the
// isActivated field) hasn't been fired. Go ahead and activate the window.
if (behavior.AssociatedObject.WindowState == WindowState.Minimized)
behavior.AssociatedObject.WindowState = WindowState.Normal;
behavior.AssociatedObject.Activate();
}
protected override void OnAttached() {
AssociatedObject.Activated += OnActivated;
AssociatedObject.Deactivated += OnDeactivated;
}
protected override void OnDetaching() {
AssociatedObject.Activated -= OnActivated;
AssociatedObject.Deactivated -= OnDeactivated;
}
void OnActivated(Object sender, EventArgs eventArgs) {
this.isActivated = true;
Activated = true;
}
void OnDeactivated(Object sender, EventArgs eventArgs) {
this.isActivated = false;
Activated = false;
}
}
The behavior requires a reference to System.Windows.Interactivity.dll. Fortunately, this is now available on NuGet in the Blend.Interactivity.Wpf package.
The behavior is attached to a Window in XAML like this:
<Window ...>
<i:Interaction.Behaviors>
<Behaviors:ActivateBehavior Activated="{Binding Activated, Mode=TwoWay}"/>
</i:Interaction.Behaviors>
The view-model should expose a boolean Activated property. Setting this property to true will activate the window (unless it is already activated). As an added bonus it will also restore a minimized window.
You could go about it in a couple of ways - adding a reference to the window could work since the viewmodel is not coupled with the view but related to it, but I don't really like that approach since it pretty much does couple your view to your viewmodel - which is not really the point of MVVM
A better approach may be to have your viewmodel raise an event or a command which the view can handle. This way the view gets to decide what UI action is associated with the command/event
e.g. simply
class SomeView
{
void HandleSomeCommandOrEvent()
{
this.Activate();
}
}
Of course how you wire this up is up to you but I'd probably try and get routed commands happening
Edit: You can't really 'bind' a simple event, since it's invoked from the viewmodel.
A simple event based example is just to add the event to the viewmodel and handle it directly ... e.g. imagine the following MainWindow with a ViewModel property
public partial class MainWindow : Window
{
MainWindowViewModel ViewModel { get; set; }
public MainWindow()
{
InitializeComponent();
ViewModel = new MainWindowViewModel();
ViewModel.ShowMessage += ViewModel_ShowMessage;
this.DataContext = ViewModel;
}
void ViewModel_ShowMessage(object sender, ShowMessageEventArgs e)
{
MessageBox.Show(e.Message, "Some caption", MessageBoxButton.OK);
}
}
Then the ViewModel can just fire the event:
// The view model
public class MainWindowViewModel
{
// The button click command
public RelayCommand ButtonClickCommand { get; set; }
// The event to fire
public event EventHandler<ShowMessageEventArgs> ShowMessage;
public MainWindowViewModel()
{
ButtonClickCommand = new RelayCommand(ButtonClicked);
}
void ButtonClicked(object param)
{
// This button is wired up in the view as normal and fires the event
OnShowMessage("You clicked the button");
}
// Fire the event - it's up to the view to decide how to implement this event and show a message
void OnShowMessage(string message)
{
if (ShowMessage != null) ShowMessage(this, new ShowMessageEventArgs(message));
}
}
public class ShowMessageEventArgs : EventArgs
{
public string Message { get; private set; }
public ShowMessageEventArgs(string message)
{
Message = message;
}
}
The XAML would be:
<Button Command="{Binding ButtonClickCommand}">Click me!</Button>
So the button invokes the command, which in turn fires the event which the view (MainWindow) handles and shows a messagebox. This way the view/UI decides on the course of action based on the type of event raised. Of course it could be your timer which fired the event
You can always go down the more involved route such as some of the answers on this question...
How should the ViewModel close the form?
but to be honest, it depends if you really need it - a simple event works well - some people overcomplicate things for the sake of elegance, but at the detriment of simplicity and productivity!
I would go this way:
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;
// View
public partial class TestActivateWindow : Window
{
public TestActivateWindow() {
InitializeComponent();
Messenger.Default.Register<ActivateWindowMsg>(this, (msg) => Activate());
}
}
// View Model
public class MainViewModel: ViewModelBase
{
ICommand _activateChildWindowCommand;
public ICommand ActivateChildWindowCommand {
get {
return _activateChildWindowCommand?? (_activateChildWindowCommand = new RelayCommand(() => {
Messenger.Default.Send(new ActivateWindowMsg());
}));
}
}
}
public class ActivateWindowMsg
{
}

Silverlight with MVVM : How to access Event of the ViewModel from the View?

I have a MVVM application and somewhere in the application our company use a Third-Party that cannot use {Binding}. It's a component that draw shapes, etc. What I want it, when the ViewModel load from the persisted storage all shapes to notify the View to draw them. In a perfect world I would just have the take the Third-party and bind it to the ViewModel Shapes collection but I cannot.
From there, my idea was that I could get from the View the ViewModel (via the DataContext) and to hook the PropertyChanged event. The problem is that the DataContext is not yet initialized in the constructor, so it's NULL, and I cannot hook the event. Here is a sample of the code:
public CanvasView()
{
InitializeComponent();
((CanvasViewModel)this.DataContext).PropertyChanged += new PropertyChangedEventHandler(CanvasView_PropertyChanged); //Exception Throw here because DataContext is null
}
void CanvasView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Shapes")
{
DrawShapes();
}
}
How can I get information from my ViewModel to my View in that case?
All of the answers so far breaks the MVVM pattern with having code-behind on the view. Personally I would wrap the 3rd party control in a UserControl and then wire up a few dependency properties with property change events.
C#
public partial class MyWrappedControl : UserControl
{
public static readonly DependencyProperty ShapesProperty = DependencyProperty.Register("Shapes", typeof(ObservableCollection<IShape>), typeof(MyWrappedControl),
new PropertyMetadata(null, MyWrappedControl.OnShapesPropertyChanged);
public ObservableCollection<IShape> Shapes
{
get { return (ObservableCollection<IShape>)GetValue(ShapesProperty); }
set { SetValue(ShapesProperty, value); }
}
private static void OnShapesPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
((MyWrappedControl)o).OnShapesPropertyChanged(e);
}
private void OnShapesPropertyChanged(DependencyPropertyChangedEventArgs e)
{
// Do stuff, e.g. shapeDrawer.DrawShapes();
}
}
XAML
<UserControl
Name="MyWrappedControl"
x:Class="MyWrappedControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<!-- your control -->
<shapeDrawerControl x:Name="shapeDrawer" />
</UserControl>
you could also attach your handler in the Loaded event.
public CanvasView()
{
InitializeComponent();
this.Loaded += this.ViewLoaded;
}
void ViewLoaded(object sender, PropertyChangedEventArgs e)
{
((CanvasViewModel)this.DataContext).PropertyChanged += new PropertyChangedEventHandler(CanvasView_PropertyChanged);
}
void CanvasView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Shapes")
{
DrawShapes();
}
}
I want to comment Dennis Roche answer.
Really, in this case we can use wrap approach, because we need to redraw view when Shapes collection changed. But view model logic can be too complex, and ,for instance, instead of redraw on PropertyChanged we should redraw on some custom event (f.i. ModelReloadEvent). In this case, wrapping doesn't help, but subscription on this event does, as in Muad'Dib solution - view model use event based communication with view, but this event should be view specific.
Using code-behind with View specific logic doesn't break MVVM. Yes, this code can be decorated with behavior/action, but using code behind - just simple solution.
Also, take a look at this view on MVVM. According to structure, ViewModel knows about abstract IView.If you worked with Caliburn/Caliburn.Micro MVVM frameworks you remember ViewAware class and IViewAware, which allows get view in view model.
So, more flexible solution I guess is next:
View:
public class CanvasView() : ICanvasView
{
public CanvasView()
{
InitializeComponent();
}
public void DrawShapes()
{
// implementation
}
}
ICanvasView:
public interface ICanvasView
{
void DrawShapes();
}
CanvasViewModel:
public class CanvasViewModel : ViewAware
{
private ObservableCollection<IShape> _shapes;
public ObservableCollection<IShape> Shapes
{
get
{
return _shapes;
}
set
{
_shapes = value;
NotifyOfPropertyChange(() => Shapes);
RedrawView();
}
}
private void RedrawView()
{
ICanvasView abstractView = (ICanvasView)GetView();
abstractView.DrawShapes();
}
}
Use the DataContextChanged event on the View (Window or UserControl)
public CanvasView()
{
InitializeComponent();
Action wireDataContext += new Action ( () => {
if (DataContext!=null)
((CanvasViewModel)this.DataContext).PropertyChanged += new PropertyChangedEventHandler(CanvasView_PropertyChanged);
});
this.DataContextChanged += (_,__) => wireDataContext();
wireDataContext();
}
void CanvasView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Shapes")
{
DrawShapes();
}
}
update: Here is a documented way to get DataContextChanged in Silverlight 3 and 4 http://www.lhotka.net/weblog/AddingDataContextChangedInSilverlight.aspx

Categories

Resources