Search code examples
wpfxamldata-binding

Cannot bind ItemsControl to ObserveableCollection


As the title states I am having a problem binding an ObservableCollection to an ItemsControl.

MainWindow.xaml:

xmlns:ext="clr-namespace:DeConfigurator.Classes"

...

<ItemsControl ItemsSource="{Binding Source=ext:InAppSink.Logs}" Name="LogList">
  <ItemsControl.ItemTemplate>
    <DataTemplate>
       <Expander Header="{Binding Level}">
         <TextBlock TextWrapping="Wrap">
            <TextBlock.Text>
               <Binding Path="FormattedEntry" />
            </TextBlock.Text>
         </TextBlock>
       </Expander>
     </DataTemplate>
   </ItemsControl.ItemTemplate>                                    

InAppSink:

namespace DeConfigurator.Classes
{
    public class InAppSink : ILogEventSink
    {
        static readonly int MAX_ITEMS = 200;
        static readonly int TO_REMOVE = 50;
        
        public ObservableCollection<LogItem> Logs { get; set; } = new ObservableCollection<LogItem>();

        public void Emit(LogEvent logEvent)
        {
            if (logEvent == null){throw new ArgumentNullException(nameof(logEvent)); }            

            if(Logs.Count > MAX_ITEMS)
            {
                for(int i = 0; i < TO_REMOVE; ++i)
                {
                    Logs.RemoveAt(i);
                }
            }

            Logs.Add(new LogItem(logEvent));            
        }
    }

    public class LogItem
    {
        readonly ITextFormatter _textFormatter =
            new MessageTemplateTextFormatter(
                "{Timestamp:yyyy-MM-dd HH:mm:ss zzz} [{Level}] [{Method}] {Message:lj}{NewLine}{Properties:j}{NewLine}{Exception}"
        );


        public string FormattedEntry { get; set; }
        public string Level { get; set; }
        public DateTime LogDate { get; set; }

        public LogItem(LogEvent logEvent)
        {
            var renderSpace = new StringWriter();
            _textFormatter.Format(logEvent, renderSpace);
            FormattedEntry = renderSpace.ToString();

            Level = logEvent.Level.ToString();
            LogDate = logEvent.Timestamp.DateTime;

        }
    }
}

The outcome I am after is to have each log entry display as a collapsed expander with the Level and date as the header and the formatted log entry as the text content when dropped. What I get is each character from the text "ext:InAppSink.Logs" is used to create the expander objects. Why is this binding not working?


Solution

  • In order to bind to a property of an object you have to set the Path of the Binding to the name of the source property, while the source object is typically passed via the DataContext of the target object of the Binding.

    For details, see Data binding overview on Microsoft Docs.


    So replace

    ItemsSource="{Binding Source=ext:InAppSink.Logs}"
    

    with

    ItemsSource="{Binding Path=Logs}"
    

    and assign an instance of the InAppSink class to the DataContext of the view, e.g. in the MainWindow constructor:

    private readonly InAppSink sink = new InAppSink();
    
    public MainWindow()
    {
        InitializeComponent();
        DataContext = sink;
    }
    

    The Logs property should also be readonly, like

    public ObservableCollection<LogItem> Logs { get; } = new ObservableCollection<LogItem>();
    

    or fire a change notification.