Ok. Forget winforms. It's useless, deprecated, ugly, it doesn't allow customization and is Slow as Hell due to lack of UI virtualization and hardware rendering.
This is my take on what you described:
<Window x:Class="MiscSamples.ThreeColumnChatSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MiscSamples"
Title="ThreeColumnChatSample" Height="300" Width="300">
<Window.Resources>
<local:FlowDocumentToXamlConverter x:Key="DocumentConverter"/>
</Window.Resources>
<ListView ItemsSource="{Binding}" ScrollViewer.HorizontalScrollBarVisibility="Hidden">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn DisplayMemberBinding="{Binding DateTime}"/>
<GridViewColumn DisplayMemberBinding="{Binding Sender}"/>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<FlowDocumentScrollViewer Document="{Binding Content, Converter={StaticResource DocumentConverter}}"
VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</Window>
Code behind:
public partial class ThreeColumnChatSample : Window
{
public ObservableCollection<ChatEntry> LogEntries { get; set; }
private string TestData = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum";
private List<string> words;
private int maxword;
public Random random { get; set; }
public ThreeColumnChatSample()
{
InitializeComponent();
random = new Random();
words = TestData.Split(' ').ToList();
maxword = words.Count - 1;
DataContext = LogEntries = new ObservableCollection<ChatEntry>();
Enumerable.Range(0, 100)
.ToList()
.ForEach(x => LogEntries.Add(GetRandomEntry()));
}
private ChatEntry GetRandomEntry()
{
return new ChatEntry()
{
DateTime = DateTime.Now,
Sender = words[random.Next(0, maxword)],
Content = GetFlowDocumentString(string.Join(" ",Enumerable.Range(5, random.Next(10, 50)).Select(x => words[random.Next(0, maxword)])))
};
}
private string GetFlowDocumentString(string text)
{
return "<FlowDocument xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>" +
" <Paragraph>" +
" <Run Text='" + text + "'/>" +
" </Paragraph>" +
"</FlowDocument>";
}
}
Data Item:
public class ChatEntry:PropertyChangedBase
{
public DateTime DateTime { get; set; }
private string _content;
public string Content
{
get { return _content; }
set
{
_content = value;
OnPropertyChanged("Content");
}
}
public string Sender { get; set; }
}
PropertyChangedBase (MVVM Helper Class):
public class PropertyChangedBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
Application.Current.Dispatcher.BeginInvoke((Action) (() =>
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}));
}
}
Result:

- I have used the
FlowDocumentToXAMLConverter
from this post
- The rich content in the third column is shown in a
FlowDocumentViewer
, but you can change that to use the bindable RichTextBox
from the linked post.
- You can resize the columns by clicking and dragging the Header edges.
- WPF has built-in UI Virtualization, which means your application will not lag horribly if there are LOTS of rows.
- You can implement the solution described here to resize the last column when resizing the containing window, thus achieving word-wrapping and resolution independence.
- Notice that most of the Code-Behind is actually boilerplate to support the example (generate random entries etc). Remove that and it's going to be a really clean solution.
- WPF Rocks. Just copy and paste my code (together with the Converter from the linked post) in a
File -> New Project -> WPF Application
and see the results for yourself.
Edit:
as per @KingKing's request, I modified my sample to emulate a chat client.
I added a reference to FsRichTextBox.dll
from the above linked CodeProject post.
<Window x:Class="MiscSamples.ThreeColumnChatSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MiscSamples"
xmlns:rtb="clr-namespace:FsWpfControls.FsRichTextBox;assembly=FsRichTextBox"
Title="ThreeColumnChatSample" WindowState="Maximized">
<Window.Resources>
<local:FlowDocumentToXamlConverter x:Key="DocumentConverter"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="300"/>
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding ChatEntries}" ScrollViewer.HorizontalScrollBarVisibility="Hidden"
x:Name="ListView">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn DisplayMemberBinding="{Binding DateTime}"/>
<GridViewColumn DisplayMemberBinding="{Binding Sender}"/>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<FlowDocumentScrollViewer Document="{Binding Content, Converter={StaticResource DocumentConverter}}"
VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
<GridSplitter Height="3" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Top"/>
<DockPanel Grid.Row="1">
<Button Content="Send" DockPanel.Dock="Right" VerticalAlignment="Bottom" Margin="2"
Click="Send_Click"/>
<rtb:FsRichTextBox Document="{Binding UserInput,Converter={StaticResource DocumentConverter}, Mode=TwoWay}"
DockPanel.Dock="Bottom" Height="300" x:Name="InputBox"/>
</DockPanel>
</Grid>
</Window>
Code Behind:
public partial class ThreeColumnChatSample : Window
{
public ChatViewModel ViewModel { get; set; }
public ThreeColumnChatSample()
{
InitializeComponent();
DataContext = ViewModel = new ChatViewModel();
}
private void Send_Click(object sender, RoutedEventArgs e)
{
InputBox.UpdateDocumentBindings();
var entry = ViewModel.AddEntry();
ListView.ScrollIntoView(entry);
}
}
ViewModel:
public class ChatViewModel:PropertyChangedBase
{
public ObservableCollection<ChatEntry> ChatEntries { get; set; }
private string _userInput;
public string UserInput
{
get { return _userInput; }
set
{
_userInput = value;
OnPropertyChanged("UserInput");
}
}
public string NickName { get; set; }
public ChatViewModel()
{
ChatEntries = new ObservableCollection<ChatEntry>();
NickName = "John Doe";
}
public ChatEntry AddEntry()
{
var entry = new ChatEntry {DateTime = DateTime.Now, Sender = NickName};
entry.Content = UserInput;
ChatEntries.Add(entry);
UserInput = null;
return entry;
}
}
Result:
