The Binding inside a Compositecollection does not work

Asked

Viewed 130 times

2

I am using a Combobox in a WPF application and when a function is activated, it loads a List<string> coming from a webservice to a property on the screen. My problem is that I can’t get Combobox to update when this property is changed (even though it is of the 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));
    }
    ...
}

example of the webservice class

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 as soon as my property gets backseja atualizada mas que mantenha o primeiroComboboxitem`, which serves as a defined initial value.

NOTE: I have already found several examples on the internet that even helped me to get to this point but none of them demonstrates how to make Binding directly from a property on my screen, they all require me to have a Resource. a Model, or something like that, and I want to do the direct Binding.

How to solve?

  • 1

    What’s wrong with using a Staticresource to bind? I’m not sure but I think it will be the only way.

  • No problem, but the point is that I need to use multiple instances of content and I see no logic in working with N-Staticresource’s... After all if it is possible to add a Compositecollecion and join a standard combo, there should be a way to update only this Collectioncontainer

  • "but the point is that I need to use multiple instances of content and I see no logic in working with N-Staticresource’s... " - Put an example in the question.

  • The my real problem is that my Combobox content is not static but dynamic, and in addition it is Reactive, so if there is an update on my webservice, it should change the Combobox options when the user is re-selecting.

  • You are making this code in the Canvas class (code Behind) or are using MVVM?

2 answers

2


You invoke that you cannot use Staticresource because Combobox is dynamic. This fact in no way invalidates its use.

So that the field itens may be "observable" It has to be property. On the other hand, you should not create a new instance but change the existing one.

Example to test:

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>

  • really in that way it works, but I’m obliged to use the <ComboBox.Resources/> to set a static Resource for the component. I will use it this way anyway, I also did not find any simpler way to do it. Thank you

1

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

https://docs.microsoft.com/en-us/dotnet/framework/wpf/data/binding-sources-overview

The properties you use as Binding source properties for a Binding must be public properties of your class. Explicitly defined interface properties cannot be accessed for Binding purposes, nor can protected, private, Internal, or virtual properties that have no base implementation.

Then we must correct the property itens being like this:

    public ObservableCollection<string> itens;

About the CompositeCollection

The problem is related to the class fact Compositecollection not be derived from a FrameworkElement and so she doesn’t have the property DataContext to support DataBinding. As we can see in the Console

System.Windows.Data Error: 2 : Cannot find Governing Frameworkelement or Frameworkcontentelement for target element.

It doesn’t make sense that this class has this kind of problem. Searching, it seems that it is an old bug and has discussions about it in 2008 (as we can see here).

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

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

Solutions:

1 - Practical solution (starting from what you already have)

Use the CollectionViewSource (proxy xaml) to fetch 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 created a template for ComboBox keeping 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>

That one TextBlock is displayed every time 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..."/>

That one Trigger is responsible for identifying when there is no value selected and display the 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 the data update. If you set a new value for property itens like you’re doing here

    itens = new ObservableCollection<string>();

Then it is necessary to use the interface INotifyPropertyChanged to update the data in the view. So:

    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 up to you... I hope it already helps xD

  • FAN-TAS-TI-CO..

Browser other questions tagged

You are not signed in. Login or sign up in order to post.