0

I have a ViewModel that is a Window, inside this Window there are many UserControls that I have made. These work fine and the bindings and DataContexts for each is set appropriately; all apart from one...

In my MainWindowView XAML I have

<Controls:LogViewerView HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
                        DataContext="{Binding LogViewerViewModel}"/>

and in my MainWindowViewModel I have

public LogViewerViewModel LogViewerViewModel { get; set; }

The LogViewerView

<UserControl x:Class="GambitFramework.Utilities.Controls.Views.LogViewerView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:Caliburn="http://www.caliburnproject.org"
             xmlns:Models="clr-namespace:GambitFramework.Utilities.Models"
             mc:Ignorable="d" 
             d:DesignHeight="300" 
             d:DesignWidth="200">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="../../Resources/Styles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>   
    </UserControl.Resources>
    <DockPanel>
        <ItemsControl ItemsSource="{Binding LogEntries}" 
                          Style="{StaticResource LogViewerStyle}">
            <ItemsControl.Template>
                <ControlTemplate>
                    <ScrollViewer CanContentScroll="True">
                        <ItemsPresenter/>
                    </ScrollViewer>
                </ControlTemplate>
            </ItemsControl.Template>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel IsItemsHost="True"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </DockPanel>
</UserControl>

where LogViewerViewModel is

public class LogViewerViewModel : PropertyChangedBase
{
    private BindableCollection<LogEntry> logEntries;

    public LogViewerViewModel() { }
    public LogViewerViewModel(IEnumerable<LogEntry> logEntries)
    {
        LogEntries = new BindableCollection<LogEntry>(logEntries);
    }

    public BindableCollection<LogEntry> LogEntries
    {
        get { return logEntries; }
        set
        {
            logEntries = value;
            NotifyOfPropertyChange(() => LogEntries);
        }
    }
}

and where in Styles.xaml we have

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:Caliburn="http://www.caliburnproject.org" 
                    xmlns:Models="clr-namespace:GambitFramework.Utilities.Models">
    <Style x:Key="LogViewerStyle" TargetType="ItemsControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <ScrollViewer CanContentScroll="True">
                        <ItemsPresenter/>
                    </ScrollViewer>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel IsItemsHost="True"/>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <DataTemplate DataType="{x:Type Models:LogEntry}">
        <Grid IsSharedSizeScope="True">
            <Grid.ColumnDefinitions>
                <ColumnDefinition SharedSizeGroup="Timestamp" Width="Auto"/>
                <ColumnDefinition SharedSizeGroup="Index" Width="Auto"/>
                <ColumnDefinition SharedSizeGroup="IconSource" Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="{Binding Timestamp}" 
                       Grid.Column="0"
                       FontWeight="Bold" 
                       Margin="5,0,5,0"/>
            <TextBlock Text="{Binding Index}" 
                       Grid.Column="1"
                       FontWeight="Bold" 
                       Margin="0,0,2,0" />
            <TextBlock Text="{Binding Message}" 
                       Grid.Column="3"
                       TextWrapping="Wrap"/>
        </Grid>
    </DataTemplate>
</ResourceDictionary>

Where the model for LogEntry is

public class LogEntry : PropertyChangedBase
{
    private uint index;
    private DateTime timestamp;
    private IconPresentor iconSource;
    private string message;

    public uint Index
    {
        get { return index; }
        set
        {
            index = value;
            NotifyOfPropertyChange(() => Index);
        }
    }

    public DateTime Timestamp
    {
        get { return timestamp; }
        set
        {
            timestamp = value;
            NotifyOfPropertyChange(() => Timestamp);
        }
    }

    public string Message
    {
        get { return message; }
        set
        {
            message = value;
            NotifyOfPropertyChange(() => Message);
        }
    }
}

But my items are not being displayed and when I use Snoop to check the bindings

Cannot set Expression. It is marked as 'NonShareable' and has already been used

which clearly suggests the DataContext is not set correctly. What am I doing wrong here and why is my DataContext not set for my control?

Thanks very much for your time.


Edit. Here is an answer using the same log control but binding to the code behind, I want to bind to a separate file: https://stackoverflow.com/a/16745054/626442

Community
  • 1
  • 1
MoonKnight
  • 23,214
  • 40
  • 145
  • 277

6 Answers6

2

I've investigated your code, and i didn't see setting DataContext for MainWindowView. There are a lot several options of doing this. For example 2 ways:

First - In your MainWindowView.xaml.cs set create and set your view model:

public MainWindow()
{
    InitializeComponent();
    DataContext = new MainWindowViewModel();
}

Second - Create and set vide model in your MainWindowView.xaml:

<Window x:Class="Test.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:test="clr-namespace:Test"
    Title="MainWindow"
    Width="525"
    Height="350">
<Window.DataContext>
    <test:MainWindowViewModel/>
</Window.DataContext>

When you do one of the above it should just work.

Also i noticed, that you noticed redundunt code int your LogViewerView, it can be just :

<DockPanel>
    <ItemsControl ItemsSource="{Binding LogEntries}" Style="{StaticResource LogViewerStyle}" />
</DockPanel>

because you have already write that code in LogViewerStyle inside your ResourceDictionary.

Hope this helps.

nrudnyk
  • 924
  • 1
  • 7
  • 25
1

Based on the statement: Edit. Here is an answer using the same log control but binding to the code behind, I want to bind to a separate file: https://stackoverflow.com/a/16745054/626442

Refer the below code to get ride of codebehind and create a viewmodel.

 public partial class MainWindow : Window
{       
    public MainWindow()
    {
        InitializeComponent();
        MainViewModel vm = new MainViewModel();
        this.DataContext = vm.LogEntries;           
    }        
}


class MainViewModel
{
    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;
    private int index;

    public ObservableCollection<LogEntry> LogEntries { get; set; }

    public MainViewModel()
    {
        random = new Random();
        words = TestData.Split(' ').ToList();
        maxword = words.Count - 1;

         LogEntries = new ObservableCollection<LogEntry>();
        Enumerable.Range(0, 200000)
                  .ToList()
                  .ForEach(x => LogEntries.Add(GetRandomEntry()));

        Timer = new Timer(x => AddRandomEntry(), null, 1000, 10);
    }

    private System.Threading.Timer Timer;
    private System.Random random;
    private void AddRandomEntry()
    {
        System.Windows.Application.Current.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() =>
        {
           LogEntries.Add(GetRandomEntry());
        }));
    }

    private LogEntry GetRandomEntry()
    {
        if (random.Next(1, 10) > 1)
        {
            return new LogEntry()
            {
                Index = index++,
                DateTime = DateTime.Now,
                Message = string.Join(" ", Enumerable.Range(5, random.Next(10, 50))
                                                     .Select(x => words[random.Next(0, maxword)])),
            };
        }

        return new CollapsibleLogEntry()
        {
            Index = index++,
            DateTime = DateTime.Now,
            Message = string.Join(" ", Enumerable.Range(5, random.Next(10, 50))
                                         .Select(x => words[random.Next(0, maxword)])),
            Contents = Enumerable.Range(5, random.Next(5, 10))
                                 .Select(i => GetRandomEntry())
                                 .ToList()
        };

    }
}

public class LogEntry : PropertyChangedBase
{
    public DateTime DateTime { get; set; }

    public int Index { get; set; }

    public string Message { get; set; }
}

public class CollapsibleLogEntry : LogEntry
{
    public List<LogEntry> Contents { get; set; }
}

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));
        }));
    }
}
Community
  • 1
  • 1
Ayyappan Subramanian
  • 5,348
  • 1
  • 22
  • 44
  • I can argue that this probably works, but in my real example above the binding/data-context is not set properly and I am essentially doing what you are... Really not sure what's going on with my code, but I can argue that the above works. However, any ideas you have as to why the production code does not bind correctly would be most appreciated... Thanks for your time. – MoonKnight Apr 04 '15 at 08:32
  • Can you share some working sample which simulates the problem? It will help in resolving your issue. – Ayyappan Subramanian Apr 05 '15 at 14:45
1

In MainWindowView you may have:

public LogViewerViewModel LogViewerViewModel { get; set; }

But that's about it. You don't state that you are initializing it to anything. With that being said, I would think you need to have something like:

public MainWindowView()
{
    LogViewerViewModel = new LogViewerViewModel();

    //Doing everything else here
}

Everything else looks good, so my only thought is you aren't initializing your view model. Check your output window for any other binding errors and what not. Make sure your collection has items in it as well.

Kcvin
  • 5,073
  • 2
  • 32
  • 54
  • Hi, I am actually initializing the ViewModel `LogViewerViewModel` correctly i.e. in the parent ViewModel's ctor I have `logViewer = new LogViewerViewModel();` or whatever. I can also see the `logViewer.LogEntries` being populaed correctly via break point. Something else (perhaps to do with the use of `DataTemplate`s?) is going on here... Thank very much for your time and happy easter! – MoonKnight Apr 04 '15 at 08:36
1

In my opinion there are several things that could help, first

public LogViewerViewModel LogViewerViewModel { get; set; }

I don't see where you have instanced this, but if it's defined too late it is not read by the view, so you can implement PropertyChangedBase in the viewmodels too (INotifiedPropertyChanged).

I am practically sure that you are having a null instead of the value.

In the case it is not check the following:

Another thing is be sure that all the properties of the customs controls are dependency properties well defined (the name of the control).

And you can test the following, instead of placing in the style place inside ItemTemplate of the itemscontrol, because I see DataTemplate directly instead of

<Setter Property="ItemTemplate"><Setter.Value>
Juan Pablo Garcia Coello
  • 3,192
  • 1
  • 23
  • 33
  • Hi, I am actually initializing the ViewModel LogViewerViewModel correctly i.e. in the parent ViewModel's ctor and fairly early such that the other controls being instantiated at the same time work. In the ctor I have `logViewer = new LogViewerViewModel();`. I can also see the `logViewer.LogEntries` being populated correctly via break point. Something else (perhaps to do with the use of `DataTemplates`?) is going on here... Thank very much for your time and happy Easter! – MoonKnight Apr 04 '15 at 08:38
1

Have you tried being explicit with your binding:

<Controls:LogViewerView HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
                        DataContext="{Binding MainWindowViewModel.LogViewerViewModel, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Views:MainWindowView}}}"/>

You'll need a namespace for the main view:

xmlns:Views="clr-namespace:GambitFramework.Utilities.Controls.Views"

Without seeing more of MainWindowView, I'd guess you've got a DataContext in between that hijacking your intent.

Byron Blattel
  • 31
  • 1
  • 3
1

You are binding the LogViewerViewModel to the DataContext of the MainWindowView instead of the DataContext of the LogViewerView

If you want to derive from parent's DataContext, have a look at similar questions like: How to access parent's DataContext from a UserControl

Notice that the DataTemplate is a bit special: https://stackoverflow.com/a/4480488

Community
  • 1
  • 1