Binding inside a CompositeCollection does not work

2

I'm using a ComboBox in a WPF application and when a function is activated, it loads a List<string> from a webservice to an on-screen property. My problem is that I can not make the ComboBox update update when this property is changed (even though it is of type ObservableCollection<string> . Ex:

screen fragment

...
<Button Content="teste" Click="carregaItens"/>

<ComboBox x:Name="cb">
  <ComboBox.ItemsSource>
    <CompositeCollection>
      <ComboBoxItem IsEnabled="False" Foreground="Gray" Content="selecione..."/>
      <CollectionContainer Collection="{Binding Path=itens}"/>
    </CompositeCollection>
  </ComboBox.ItemsSource>
</ComboBox>
...

Screen Class Fragment

public partial class Tela: Window
{
    ...
    private ObservableCollection<string> itens;
    private async void carregaItens(object sender, RoutedEventArgs e)
    {
        itens = new ObservableCollection<string>();
        (await new WebService().getItens()).ForEach(x => itens.Add(x));
    }
    ...
}

webservice class example

public class Webservice
{
    public async Task<List<string>> getItens()
    {
        return Task.Run(() => new List<string>(){ "foo", "bar", "bin" });
    }
}

I need the ComboBox % update your content so my property items seja atualizada mas que mantenha o primeiro % ComboBoxItem ', which serves as an initial value set.

  

Note: has found several examples on the internet that even helped me get to this point but none of them shows how to direct binding of a property on my screen, all of them require me to have a Resource. a Model, or something like that, and I want to do direct binding.

How to solve?

    
asked by anonymous 27.03.2018 / 17:50

2 answers

2

You invokes that you can not use StaticResource because the ComboBox is dynamic . This in no way invalidates its use.

In order for the itens field to be "observable" it must be a property. On the other hand, you should not create a new instance but change the existing one.

Test example:

C #

public partial class MainWindow
{
    public ObservableCollection<string> Itens { get; } = new ObservableCollection<string>();

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
    }

    void CarregaItens(object sender, RoutedEventArgs e)
    {
        Itens.Clear();
        GetItens().ForEach(x => Itens.Add(x));
    }

    //Simula o acesso ao sefvidor
    private static List<String> GetItens()
    {
        var rand = new Random();
        return new List<string>
        {
            rand.Next(1, 10).ToString(),
            rand.Next(1, 10).ToString(),
            rand.Next(1, 10).ToString()
        };
    }
}

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">

    <Grid>
        <StackPanel>
            <Button Content="teste" Click="CarregaItens"/>

            <ComboBox x:Name="cb">
                <ComboBox.Resources>
                    <CollectionViewSource x:Key="itens" Source="{Binding Itens}"/>
                </ComboBox.Resources>
                <ComboBox.ItemsSource>
                    <CompositeCollection>
                        <ComboBoxItem IsEnabled="False" Foreground="Gray" Content="selecione..."/>
                        <CollectionContainer Collection="{Binding Source={StaticResource itens}}" />
                    </CompositeCollection>
                </ComboBox.ItemsSource>
            </ComboBox>
        </StackPanel>
    </Grid>

    
28.03.2018 / 22:57
1

First of all, one of the requirements of binding is the referenced property to be public.

link

  

The properties you use for the binding properties for a binding must be public properties of your class. Explicitly defined interface properties can not be accessed for binding purposes, nor can protected, private, internal, or virtual properties that have in base implementation.

In this case we must correct the itens property, thus:

    public ObservableCollection<string> itens;

About CompositeCollection

The problem is related to the fact that the class CompositeCollection is not derived from a FrameworkElement and therefore it does not have the DataContext property to support DataBinding . As we can see in Console

  

System.Windows.Data Error: 2: Can not find governing FrameworkElement or FrameworkContentElement for target element.

It does not make sense for this class to have this kind of problem. Researching it looks like it is an old bug and has discussions about it in 2008 ( as we can see here ).

(I did tests on .NET FRAMEWORK 4.7.1 and the bug still exists.)

One of the solutions is to use a kind of proxy ( quoted in this blog in 2011 )

Solutions:

1 - Practical solution (from what you already have)

Use CollectionViewSource (proxy xaml) to fetch the data:

<ComboBox SelectedIndex="0" >
        <ComboBox.Resources>
            <CollectionViewSource x:Key="itens" Source="{Binding itens}"/>
        </ComboBox.Resources>
        <ComboBox.ItemsSource>
            <CompositeCollection>
                <ComboBoxItem IsEnabled="False" Foreground="Gray" Content="selecione..."/>
                <CollectionContainer Collection="{Binding Source={StaticResource itens}}" />
            </CompositeCollection>
        </ComboBox.ItemsSource>
    </ComboBox>

2 - More complicated solution (ideal for better quality)

I've created a template for ComboBox while retaining the default style (you can customize this to the level you want).

<ControlTemplate x:Key="ComboBoxTemplate" TargetType="{x:Type ComboBox}">
        <Grid x:Name="MainGrid" SnapsToDevicePixels="true">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/>
            </Grid.ColumnDefinitions>

            <Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Placement="Bottom">
                <Themes:SystemDropShadowChrome x:Name="Shdw" Color="Transparent" MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{Binding ActualWidth, ElementName=MainGrid}">
                    <Border x:Name="DropDownBorder" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}">
                        <ScrollViewer x:Name="DropDownScrollViewer">
                            <Grid RenderOptions.ClearTypeHint="Enabled">
                                <Canvas HorizontalAlignment="Left" Height="0" VerticalAlignment="Top" Width="0">
                                    <Rectangle x:Name="OpaqueRect" Fill="{Binding Background, ElementName=DropDownBorder}" Height="{Binding ActualHeight, ElementName=DropDownBorder}" Width="{Binding ActualWidth, ElementName=DropDownBorder}"/>
                                </Canvas>
                                <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            </Grid>
                        </ScrollViewer>
                    </Border>
                </Themes:SystemDropShadowChrome>
            </Popup>
            <ToggleButton BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ComboBoxReadonlyToggleButton}"/>
            <ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Content="{TemplateBinding SelectionBoxItem}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
            <TextBlock x:Name="ExibeSelecione" HorizontalAlignment="Left" Margin="5,0,0,0" VerticalAlignment="Center" Visibility="Hidden" IsEnabled="True" Foreground="Gray" Text="Selecione..."/>
        </Grid>
        <ControlTemplate.Triggers>
            <Trigger Property="HasDropShadow" SourceName="PART_Popup" Value="true">
                <Setter Property="Margin" TargetName="Shdw" Value="0,0,5,5"/>
                <Setter Property="Color" TargetName="Shdw" Value="#71000000"/>
            </Trigger>
            <Trigger Property="HasItems" Value="false">
                <Setter Property="Height" TargetName="DropDownBorder" Value="95"/>
            </Trigger>
            <Trigger Property="IsEnabled" Value="false">
                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                <Setter Property="Background" Value="#FFF4F4F4"/>
            </Trigger>
            <Trigger Property="SelectedItem" Value="{x:Null}">
                <Setter Property="Visibility" Value="Visible" TargetName="ExibeSelecione"/>
            </Trigger>
            <MultiTrigger>
                <MultiTrigger.Conditions>
                    <Condition Property="IsGrouping" Value="true"/>
                    <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
                </MultiTrigger.Conditions>
                <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
            </MultiTrigger>
            <Trigger Property="ScrollViewer.CanContentScroll" SourceName="DropDownScrollViewer" Value="false">
                <Setter Property="Canvas.Top" TargetName="OpaqueRect" Value="{Binding VerticalOffset, ElementName=DropDownScrollViewer}"/>
                <Setter Property="Canvas.Left" TargetName="OpaqueRect" Value="{Binding HorizontalOffset, ElementName=DropDownScrollViewer}"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

This TextBlock is displayed whenever no value is selected

                <TextBlock x:Name="ExibeSelecione" HorizontalAlignment="Left" Margin="5,0,0,0" VerticalAlignment="Center" Visibility="Hidden" IsEnabled="True" Foreground="Gray" Text="Selecione..."/>

This Trigger is responsible for identifying when there is no value selected and displaying TextBlock

 <Trigger Property="SelectedItem" Value="{x:Null}">
                <Setter Property="Visibility" Value="Visible" TargetName="ExibeSelecione"/>
  </Trigger>

- Edit -

@LeandroLuk The use of Collection="{Binding Source={StaticResource itens}} is unrelated to data refresh. If you are going to set a new value for itens property like you are doing here

    itens = new ObservableCollection<string>();

Then you need to use the INotifyPropertyChanged interface to update the data in the view. Staying like this:

    private ObservableCollection<string> _itens;
    public ObservableCollection<string> itens { get { return _itens; } set { _itens = value; NotifyPropertyChanged(); } }

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private async void carregaItens(object sender, RoutedEventArgs e)
    {
        itens = new ObservableCollection<string>();

        var result = await new WebService().getItens();
        result.ForEach(x => itens.Add(x));
    }

The rest is with you ... I hope this already helps xD

    
28.03.2018 / 19:47