none
UWP Binding versagt beim zweiten Aufruf RRS feed

  • Frage

  • Ich habe eine winzige App geschrieben in der ich eigentlich etwas sehr simples tun will, nämlich in einer ComboBox eine ObservableCollection die in einer Klasse steckt anzeigen, den ersten Eintrag vorauswählen und in einer zweiten ComboBox eine ObservableCollection anzeigen die in den einzelnen Items der ersten ObservableCollection stecken. Also namentlich simpel MainSystem und SubSystem. Ich mache das ganze in einer UWP App, daher kann ich einige standardsachen aus .NET nicht verwenden. Unter anderem funktioniert die ComboBox Property 'IsSynchronizedWithCurrentItem="True"' nicht. Da bekomme ich dann die Fehlermeldung "Fehler beim Festlegen von...".

    Mein XAML sieht so aus:

        <Page.DataContext>
            <viewmodels:EditRulesViewModel />
        </Page.DataContext>

    ...

    <StackPanel Orientation="Horizontal"> <StackPanel> <TextBlock FontWeight="Bold">Regelsysteme</TextBlock> <StackPanel Orientation="Horizontal"> <ComboBox x:Name="MainSystemComboBox" ItemsSource="{Binding Path=RuleSet.RuleMainSystems}" SelectedItem="{Binding Path=SelectedMainSystem, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" DisplayMemberPath="Name" SelectedValuePath="Id" /> <TextBox x:Name="MainSystemTextBox" MinWidth="200" /> <Button Content="Hinzufügen" /> </StackPanel> </StackPanel> <StackPanel Margin="50,0,0,0"> <TextBlock FontWeight="Bold">Subsytem</TextBlock> <StackPanel Orientation="Horizontal"> <ComboBox x:Name="SubSystemComboBox" ItemsSource="{Binding Path=SelectedMainSystem.RuleSubSystems}" SelectedItem="{Binding Path=SelectedSubSystem, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" DisplayMemberPath="Name" SelectedValuePath="Id" /> <TextBox x:Name="SubSystemTextBox" MinWidth="200" /> <Button Content="Hinzufügen" /> </StackPanel> </StackPanel> </StackPanel>


    Und natürlich das ViewModel:

        public class EditRulesViewModel : INotifyPropertyChanged
        {
            private RuleSet _ruleSet = new RuleSet();
            private RuleMainSystem _selectedMainSystem;
            private RuleSubSystem _selectedSubSystem;
    
            public RuleSet RuleSet
            {
                get => _ruleSet;
                set
                {
                    _ruleSet = value;
                    OnPropertyChanged();
                }
            }
    
            public RuleMainSystem SelectedMainSystem
            {
                get => _selectedMainSystem;
                set
                {
                    _selectedMainSystem = value;
                    SelectedSubSystem = SelectedMainSystem.RuleSubSystems.FirstOrDefault();
                    OnPropertyChanged();
                }
            }
    
            public RuleSubSystem SelectedSubSystem
            {
                get => _selectedSubSystem;
                set
                {
                    // Beim zweiten Auruf kommt hier ein NULL wert an, aber auch ein Anfangen hebt den Fehler nicht auf.
                    //if (value == null)
                    //    return;
                    _selectedSubSystem = value;
                    OnPropertyChanged();
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
    
            public EditRulesViewModel()
            {
                // Füllen der ObservableCollections
                InitializeRules();
                // Auswählen des ersten Eintrags in der linken ComboBox
                SelectedMainSystem = RuleSet.RuleMainSystems.FirstOrDefault();
            }

    Wie schon beschrieben wird durch den letzten Eintrag 'SelectedMainSystem = ...' die linke ComboBox mit dem ersten Eintrag der Liste gefüllt und dabei wird auch wie man oben sieht durch 'SelectedSubSystem = ...' der erste Eintrag in der zweiten ComboBox ausgewählt. Bei diesem Aufruf findet ebenfalls, wenn man mit F11 durchgeht, ein Aufruf der Methode 'GetXamlType' der automatisch generierten Datei 'XamlTypeInfo.g.cs' statt. Beim zweiten Ändern des Eintrags der ComboBox (dabei ist es egal ob durch programmatisches Setzen des SelectedMainSystem oder durch Auswahländerung in der ComboBox) findet dieser Aufruf nicht mehr statt und die zweite ComboBox ändert zwar korrekterweise ihren Inhalt, wählt aber keinen Eintrag als Vorbelegung aus.

    Hat jemand eine Idee woran das genau liegen könnte oder wie man das unter UWP korrekterweise lösen könnte?

    Freitag, 1. Juni 2018 07:15

Antworten

  • Hi Marcel,
    unabhängig von den von Thomas genannten Empfehlungen, die berücksichtigt werden sollten, liegt die Ursache Deines Problems darin, dass die zweite Combobox zum Zeitpunkt der Zuweisung von SerlectedSubSystem die aktuelle Liste noch nicht kennt. Du solltest also zuerst die aktuelle Liste zuweisen (über PropertyChanged) und dann den ersten Eintrag:

        public RuleMainSystem SelectedMainSystem
        {
          get => _selectedMainSystem;
          set
          {
            _selectedMainSystem = value;
            OnPropertyChanged();
            SelectedSubSystem = SelectedMainSystem?.RuleSubSystems.FirstOrDefault();
          }
        }

     

    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks


    Freitag, 1. Juni 2018 14:49

Alle Antworten

  • Hallo,

    ich würde bei UWP nicht alle Events bin ins ViewModel durchschleifen. Zudem sollte man bei UWP x:Bind nutzen. Siehe hierfür diesen Link

    Dein Problem würde ich so lösen.

    XAML:

    <Page
        x:Class="App2.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:App2"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
    
            <ComboBox ItemsSource="{x:Bind ViewModel.MainCollection, Mode=OneWay}" SelectionChanged="MainComboBox_SelectionChanged">
                <ComboBox.ItemTemplate>
                    <DataTemplate x:DataType="local:MyMainModel">
                        <TextBlock Text="{x:Bind Name}" />
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
    
            <ComboBox Grid.Column="1" ItemsSource="{x:Bind ViewModel.SubCollection, Mode=OneWay}">
                <ComboBox.ItemTemplate>
                    <DataTemplate x:DataType="local:MySubModel">
                        <TextBlock Text="{x:Bind Name}" />
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
        </Grid>
    </Page>

    C#

    namespace App2
    {
        /// <summary>
        /// An empty page that can be used on its own or navigated to within a Frame.
        /// </summary>
        public sealed partial class MainPage : Page
        {
            public MyViewModel ViewModel { get; set; }
    
            public MainPage()
            {
                this.InitializeComponent();
    
                ViewModel = new MyViewModel();
            }
    
            private void MainComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                if (e.AddedItems[0] is MyMainModel m)
                {
                    ViewModel.SubCollection.Clear();
                    foreach (var item in m.Items)
                    {
                        ViewModel.SubCollection.Add(item);
                    }
                }
            }
        }
    
        public class MyViewModel : INotifyPropertyChanged
        {
            public MyViewModel()
            {
                
                MainCollection = new ObservableCollection<MyMainModel>();
                for (int i = 0; i < 10; i++)
                {
                    List<MySubModel> list = new List<MySubModel>();
                    for (int ii = 0; ii < 10; ii++)
                    {
                        list.Add(new MySubModel() { Name = $"Main-{i} - Sub-{ii}" });
                    }
                    MainCollection.Add(new MyMainModel() { Name = $"Main-{i}", Items = list });
                }
                SubCollection = new ObservableCollection<MySubModel>();
            }
    
            public ObservableCollection<MyMainModel> MainCollection { get; set; }
            public ObservableCollection<MySubModel> SubCollection { get; set; }
            
            public event PropertyChangedEventHandler PropertyChanged = delegate { };
            public void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                // Raise the PropertyChanged event, passing the name of the property whose value has changed.
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    
        public class MyMainModel
        {
            public string Name { get; set; }
            public List<MySubModel> Items { get; set; }
        }
    
        public class MySubModel
        {
            public string Name { get; set; }
        }
    }

    Beachte bitte das Du die ObservableCollection niemals neu erzeugen darfst. Deswegen mache ich nur ein Clear und füge die neuen Elemente der SubCollection in einer Schleife hinzu.

    Gruß Thomas


    13 Millionen Schweine landen jährlich im Müll
    Dev Apps von mir: Icon für UWP, UI Strings
    Andere Dev Apps: UWP Community Toolkit Sample App

    Freitag, 1. Juni 2018 12:47
  • Hi Marcel,
    unabhängig von den von Thomas genannten Empfehlungen, die berücksichtigt werden sollten, liegt die Ursache Deines Problems darin, dass die zweite Combobox zum Zeitpunkt der Zuweisung von SerlectedSubSystem die aktuelle Liste noch nicht kennt. Du solltest also zuerst die aktuelle Liste zuweisen (über PropertyChanged) und dann den ersten Eintrag:

        public RuleMainSystem SelectedMainSystem
        {
          get => _selectedMainSystem;
          set
          {
            _selectedMainSystem = value;
            OnPropertyChanged();
            SelectedSubSystem = SelectedMainSystem?.RuleSubSystems.FirstOrDefault();
          }
        }

     

    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks


    Freitag, 1. Juni 2018 14:49
  • Ähm, danke für die Antwort, aber auch nach deinem Schema wird KEINE Vorbelegung der zweiten ComboBox getroffen. Mit den richtigen Daten wird sie ja schon gefüllt und auch wenn Binding langsamer ist als x:Bind funktioniert es ja. Aber es geht ja darum dass der Aufruf der Vorbelegung nicht getroffen wird.

    Und würde ich die Einträge in der ComboBox (oder in dem Fall der OC) per Hand rauswerfen und neu hinzufügen dann würden ja die Bindings keinen Sinn mehr ergeben. Da könnte ich ja wie in alten Zeiten ComboBoxItems per Hand erzeugen, etc.

    So langsam glaube ich dass UWP an sich eine total besch...eidene Idee von MS war. 

    @Peter: Hatte ich zwischendurch auch ausprobiert, änder aber (in meiner Konstellation) nichts am Problem.


    edit: MIST! Ok, Peters Antwort war doch richtig... warum kann ich aber nicht sagen, weil ich genau das schon ausprobiert hatte... hm... echt komisch... aber dann trotzdem danke.
    Dienstag, 5. Juni 2018 09:37
  • Hi Marcel,
    dass Binding langsamer ist als x:Bind kann man bei den derzeitigen CPU's vernachlässigen. Viel wichtiger ist der Verbrauch an Energie. UWP ist hauptsächlich gedacht für Geräte, die mit der Akku-Leistung sparsam umgehen sollten. Und da bringt x:Bind gewaltige Vorteile.

    Was Du unter "ComboBoxen per Hand erzeugen" verstehst, kann ich mir nicht vorstellen. Auch "Einträge per Hand" rauswerfen ist unklar. In jedem Fall wird eine Combobox per Code erzeugt ("gemalt"). Listenträge in der ComboBox sind in jedem Fall auch nur Elemente einer Liste, die entweder explizit zugewiesen wird (ItemsSource) oder implizit als Items (-List) verwaltet wird.

    Wenn Du für Windows-Desktop-Systeme programmierst, brauchst Du kein UWP. Für kleine Geräte ist UWP jedoch recht vorteilhaft, vor allem auch wegen dem geringeren Funktionsumfang, der bedeutend weniger Energie verbraucht. Warum Du UWP nicht so prickelnd findest, kann ich nicht nachvollziehen. Willst Du einen 1000 PS-Motor in einem Smart haben?

    Warum meine Antwort richtig war, hatte ich doch geschrieben. Du hattest zuerst ein Element aus eine Liste zugewiesen, die die Combobox noch nicht kannte und dann erst die dazu gehörende Liste der Combobox zugewiesen. Umgekehrt ist es richtig. Zuerst die Liste zuweisen und dann mitteilen, welches Element dieser Liste auszuwählen ist.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks

    Dienstag, 5. Juni 2018 10:22
  • Ist ja auch alles richtig. Aber ich bin hier gerade in einer Fortbildung und MUSS UWP lernen. Ich möchte aber später wieder in der Desktopentwicklung arbeiten. Da würde ich dann wohl wahrscheinlich mit WPF arbeiten. Das hat große Ähnlichkeit mit UWP, ist aber schöner (meiner Meinung nach).

    Was ich mit "per Hand" meine ist so wie man es damals mal ohne Bindings gemacht hat. Also sowas wie:

    <ComboBox Name="MyComboBox" />

    und dann

    foreach (string name in stringList)
    {
        ComboBoxItem newItem = new ComboBoxItem();
        newItem.Content = name;
        MyComboBox.ItemList.Add(newItem);
    }

    Das wäre ja dann vom Aufwand her auch extrem und nicht wirklich im Sinne vom Binding.

    Dienstag, 12. Juni 2018 08:43
  • Der Sinn von Binding ist die UI vom Code zu trennen damit ein Grafiker und ein Coder zusammen und gleichzeitig an einem Projekt zusammenarbeiten können. Zudem wird erst in der UI festgelegt wie die Daten angezeigt werden. Dafür braucht es ein Interface (ViewModel) in dem Beschrieben wird was für Daten von und zu UI gehen. Wann und in welcher Codedatei das ViewModel mit Daten gefüllt wird, spielt dabei keine Rolle.

    Legt man nun im Team fest das in der CodeBehind kein Code zu stehen hat und alles ans ViewModel weitergeleitet werden muss, sollte man sich daran halten. 

    Dennoch sollte man immer in Betracht ziehen das einen neue Plattform auch andere Anforderungen hat. Einer Plattform immer das selbe Pattern aufzuzwingen macht wenig sinn.

    Es ist ja auch nicht so das MVVM wie es unter WPF genutzt wird das non plus ultra ist. Es wird immer neue Ansätze geben und WPF wird nicht ewig bleiben. 

    Es wird auch in ein paar Jahren Schluss mit dem .NET Framework sein und MS entwickelt nur noch .NET Core. 2 gleisig zu fahren macht auf Dauer wenig sinn.

    Ich bin Softwareentwickler geworden weil ich es super finde neues zu lernen. Meinetwegen könnte MS jedes Jahr ein neues Framework rausbringen und die alten einstampfen. Hauptsache sie bringen einen Mehrwert mit.


    Gruß Thomas
    13 Millionen Schweine landen jährlich im Müll
    Dev Apps von mir: UWP Segoe MDL2 Assets, UI Strings


    Mittwoch, 13. Juni 2018 03:40