How to Display ObservableCollection<string> in a UserControl
Asked Answered
E

4

3

I'm new to WPF and I've found some similar questions but can't quite figure out the last part. I have a ViewModel with an ObservableCollection that contains error messages. I want to display these on the form AND allow the user to select and copy all or part of the messages. (In the past in WinForm apps I used a RichTextBox for this, but I can't figure out how to bind to one to the collection in WPF.)

I got the look I was after with the following xaml, but there is no built-in way to select and copy like I could with a RichTextBox. Does anyone know which control I should use or if there is way to enable selecting/copying the contents of all the TextBlocks, or a way to bind this to a RichTextBox?

    <Grid Margin="6">
    <ScrollViewer VerticalScrollBarVisibility="Auto"  Height="40" Grid.Column="0" Margin="6">
        <ItemsControl ItemsSource="{Binding ErrorMessages}" >            
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                     <TextBlock Text="{Binding Mode=OneWay}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>
</Grid>

[Edit] @Andrey Shvydky - This wouldn't fit in a comment. It took me a while to figure out the proper syntax (especially the /, thing) but eventually I ended up with the Flow Document syntax shown below. It looks correct on the form and at first seems to support select all/copy. But when I paste after a select all/copy nothing ever shows up. Anyone know why?

 <Grid Margin="6">
    <FlowDocumentScrollViewer>
        <FlowDocument >
            <Paragraph>
                <ItemsControl ItemsSource="{Binding ErrorMessages, Mode=OneWay}" />
                <Run Text="{Binding /, Mode=OneWay}" />
            </Paragraph>
        </FlowDocument>
    </FlowDocumentScrollViewer>
</Grid>
Edette answered 14/6, 2011 at 5:15 Comment(0)
B
1

May be usefull to generate FlowDocument and show this document in FlowDocumentReader. Try to start from this article: Flow Document Overview.

Example of generation:

    void ShowErrors(FlowDocumentReader reader, Exception[] errors) {
        FlowDocument doc = new FlowDocument();
        foreach (var e in errors) {
            doc.Blocks.Add(new Paragraph(new Run(e.GetType().Name)) {
                Style = (Style)this.FindResource("header")
            });
            doc.Blocks.Add(new Paragraph(new Run(e.Message)) {
                Style = (Style)this.FindResource("text")
            });
        }
        reader.Document = doc;
    }

In this example I have added some styles for text in flowdocument. PLease look at XAML:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <Style x:Key="header" TargetType="{x:Type Paragraph}">
        <Setter Property="FontWeight" Value="Bold"/>
    </Style>
    <Style x:Key="text" TargetType="{x:Type Paragraph}">
        <Setter Property="Margin" Value="30, 0, 0, 0"/>
    </Style>
</Window.Resources>
<FlowDocumentReader Name="reader">
</FlowDocumentReader>

Result:

enter image description here

Bevel answered 14/6, 2011 at 5:22 Comment(3)
Took me a while to figure out the correct syntax (especially the /, but eventually I came up with <FlowDocumentScrollViewer> <FlowDocument > <Paragraph> <ItemsControl ItemsSource="{Binding ErrorMessages, Mode=OneWay}" /> <Run Text="{Binding /, Mode=OneWay}" /> </Paragraph> </FlowDocument> </FlowDocumentScrollViewer>Edette
FlowDocument doesn't support binding. In this case you should generate document in code and assign it to Document property of FlowDocumentReader. This is very similar to approach with RichTextBox in WinForms. Of course you can use ItemControl too but FlowDocumentReader is the simple way to achive Select/Copy functionality.Bevel
I meant to delete my comment here. It was too hard to read and it didn't all fit so I added an edit to my original post. I didn't realize that FlowDocument didn't directly support binding directly, which I find kind of strange because the <Run> element does support binding. Based on your suggestion I have it looking great, I just can't paste the results and I have no idea why. I can select all, and copy and it appears to work, but when I paste nothing shows up.Edette
D
2

Unless you have a great amount of messages a simple converter might be viable:

<TextBox IsReadOnly="True">
    <TextBox.Text>
        <Binding Path="Messages" Mode="OneWay">
            <Binding.Converter>
                <vc:JoinStringsConverter />
            </Binding.Converter>
        </Binding>
    </TextBox.Text>
</TextBox>
public class JoinStringsConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var strings = value as IEnumerable<string>;
        return string.Join(Environment.NewLine, strings);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}
Drusilladrusus answered 14/6, 2011 at 5:24 Comment(1)
Since I'm learning, this sounds like a good way to learn about and write my first converter. Thanks for the suggestion and code.Edette
B
1

May be usefull to generate FlowDocument and show this document in FlowDocumentReader. Try to start from this article: Flow Document Overview.

Example of generation:

    void ShowErrors(FlowDocumentReader reader, Exception[] errors) {
        FlowDocument doc = new FlowDocument();
        foreach (var e in errors) {
            doc.Blocks.Add(new Paragraph(new Run(e.GetType().Name)) {
                Style = (Style)this.FindResource("header")
            });
            doc.Blocks.Add(new Paragraph(new Run(e.Message)) {
                Style = (Style)this.FindResource("text")
            });
        }
        reader.Document = doc;
    }

In this example I have added some styles for text in flowdocument. PLease look at XAML:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <Style x:Key="header" TargetType="{x:Type Paragraph}">
        <Setter Property="FontWeight" Value="Bold"/>
    </Style>
    <Style x:Key="text" TargetType="{x:Type Paragraph}">
        <Setter Property="Margin" Value="30, 0, 0, 0"/>
    </Style>
</Window.Resources>
<FlowDocumentReader Name="reader">
</FlowDocumentReader>

Result:

enter image description here

Bevel answered 14/6, 2011 at 5:22 Comment(3)
Took me a while to figure out the correct syntax (especially the /, but eventually I came up with <FlowDocumentScrollViewer> <FlowDocument > <Paragraph> <ItemsControl ItemsSource="{Binding ErrorMessages, Mode=OneWay}" /> <Run Text="{Binding /, Mode=OneWay}" /> </Paragraph> </FlowDocument> </FlowDocumentScrollViewer>Edette
FlowDocument doesn't support binding. In this case you should generate document in code and assign it to Document property of FlowDocumentReader. This is very similar to approach with RichTextBox in WinForms. Of course you can use ItemControl too but FlowDocumentReader is the simple way to achive Select/Copy functionality.Bevel
I meant to delete my comment here. It was too hard to read and it didn't all fit so I added an edit to my original post. I didn't realize that FlowDocument didn't directly support binding directly, which I find kind of strange because the <Run> element does support binding. Based on your suggestion I have it looking great, I just can't paste the results and I have no idea why. I can select all, and copy and it appears to work, but when I paste nothing shows up.Edette
D
1

Simplest way:

Assuming your viewmodel implements INotifyPropertyChange, create an event handler for the ObservableCollection PropertyChanged event. Create a property which aggregates all of the items in the observable colleciton into a single string. Whenever the observable collection changes, fire off a notification event for your new property. Bind to that property

public class ViewModel : INotifyPropertyChange
{
    public ViewModel()
    {
        MyStrings.CollectionChanged += ChangedCollection;
    }
    public ObservableCollection<string> MyStrings{get;set;}

    public void ChangedCollection(args,args)
    {
        base.PropertyChanged("MyAllerts");
    }

    public string MyAllerts
    {
        get
        {
            string collated = "";
            foreach(var allert in MyStrings)
            {
                collated += allert;
                    collated += "\n";
            }
        }
    }

}

I know this code is fraught with errors (i wrote it in SO instead of VS), but it should give you some idea.

Doorstop answered 14/6, 2011 at 5:23 Comment(3)
Thanks, I guess I was thinking I was missing something obvious but this seems like a reasonable work-around. Since I'm still learning I probably take the similar suggestion below and try to learn more about creating a converter.Edette
Raw foreach string concatenation is not something you should do, use a StringBuilder otherwise a new string will be created in every iteration. (Or just use String.Join which does all that for you internally)Drusilladrusus
Thanks HB. Didnt know about string.join, usually use a string builder. Was just using a quick and dirty example. is string.join actually efficient or is it just a masked foreach?Doorstop
H
0
<Grid Margin="6">
    <ScrollViewer VerticalScrollBarVisibility="Auto"  Height="40" Grid.Column="0" Margin="6">
        <ItemsControl ItemsSource="{Binding ErrorMessages}" >            
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                     <TextBox Text="{Binding ViewModelMemberRepresentingYourErrorMessage}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>
</Grid>
Halfbaked answered 14/6, 2011 at 5:18 Comment(2)
I'm not sure how this would help. All my error messages show up correctly in my code (I just can't copy and past them). I don't have a member that represents an individual error message, just the collection. I may not understand WPF well enough to understand your point.Edette
@Tod: There is no explanation and i don't see any point in this either.Drusilladrusus

© 2022 - 2024 — McMap. All rights reserved.