Binding/DataContext Issue with ItemSource in WPF u

2019-06-14 11:46发布

问题:

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

回答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



回答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.



回答3:

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));
        }));
    }
}


回答4:

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.



回答5:

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>


回答6:

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.