WPF und RoutedEventArgs



  • Nimm den VisualTreeHelper, damit kannst du rauf und runter klettern.
    Warum es ein Designfehler ist? Weil man nicht direkt mit UI Controls arbeiten sollte. Du hast dadurch eine viel zu feste Verbindung zwischen Ausgabe und Logik.



  • Mit was sollte man dann Arbeiten?



  • Mit Entwurfsmustern wie MVP, MVC oder ideal für WPF das MVVM.
    Egal welches du nimmst, das Ziel sollte sein Ausgabe und Logik strikt voneinander zu trennen. Dadurch ist es nicht nur leicht veränderbar, sondern auch vollständig testbar.
    Aus unit tests heraus gibt es keine UI Controls, entsprechend wären die Methoden wenn sie damit arbeiten nicht mehr ausführbar. Darum trennt man es strikt.



  • Ok. Danke werde mir das mal durchlesen und einprägen.
    Aber sei mir leise mit unit tests. Meiner Meinung nach das unnötigste was es gibt. 🙂



  • Hoffe die lange Beschreibung schreckt nicht ab.

    Ich will mich ja nun nicht selber loben. Aber nachdem ich mich nun etwas mit MVVM beschäftigt habe musste ich feststellen dass ich mit meinem Programm selber gar nicht so weit weg bin.

    Habe verschiedene Ansichten die ich sogar alle mit View bezeichnet habe. Ausser ein paar DepencyProperties ist im Codebehind nichts implementiert. Desweiteren habe ich eine Klasse die die Definition der Daten darstellt. Im MVVM würde man diese ja Model nennen. Und eine Klasse die Objekte dieser Klasse anlegt und verwaltet. Diese Daten sind dann wiederum mit Databinding an die Views gebunden.

    Nun noch mal zu meiner Ausgangsfrage dieses Threads. Ich habe eine Overview in der ich mehrere Controls also Views anzeige. Es handelt sich immer um das selbe View.

    Wir könnten zum Beispiel zum leichteren Verständnis ein Beispiel nehmen. Bei meinen Daten handelt es sich um Personen. Ein einzelnes View zeigt nun z.B. Vor und Nachname einer Person an. In meiner Übersicht hätte ich nun gerne z.B. 10 dieser Views nebeneinander angezeigt. Wenn ich nun einen Button einer dieser Views klicke möchte ich in die Detailansicht dieser Person springen.

    So und nun sollte ich wissen welche Person angeklickt wurde. In OrginalSource steht nun nur Button und in Source steht Overview.

    Ich sollte nun aber wissen welche Person angeklickt wurde. Und das ist nun die Frage wie ich das rausfinde. Und zwar so dass es in das MVVM passt.



  • Ich gehe davon aus das du ein "Person" Objekt hast und eine Liste von Person Objekten an die Oberfläche bindest.
    Mit nem ItemsPanelTemplate (z.B. WrapPanel) und ItemTemplate (Direkt oder UserControl) lässt du die Objekte dann anzeigen.

    Mal Pseudocode was ich meinte (Alles komplett hier im Forum getippt):

    Variante 1:
    Wenn du nun ein Button in einer Person an klickst, schickst du ein Command. Dieses Command Binding biegst du mit RelativeSource zum Fenster um und schickst als CommandParameter die jeweilige Person mit.

    public class MainViewModel : ObservableObject
    {
        public MainViewModel()
        {
            DoAnythingCommand = new DelegateCommand(p => DoAnything(p));
    
            Persons = new EnhancedObservableCollection<Person>();
            FillPersons(PersonRepository.CreatePersons());
        }
    
        private void FillPersons(IEnumerable<Person> persons)
        {
            foreach (var person in persons)
                Persons.Add(person);
        }
    
        public EnhancedObservableCollection<Person> Persons { get; set; }
    
        public ICommand DoAnythingCommand { get; set; }
        private void DoAnything(object e)
        {
            var person = e as Person;
            if (person != null)
            {
                // do anything with person
            }
        }
    }
    
    <Window x:Class"...MainView">
        <ItemsControl ItemsSource="{Binding Persons}">
            <ItemsContro.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Button Content="Do Anything"
                                Command="{Binding DataContext.DoAnythingCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                                CommandParmater="{Binding }" />
                    </Grid>
                </DataTemplate>
            </ItemsContro.ItemTemplate>
        </ItemsControl>
    </Window>
    

    Varainte 2:
    Du hast pro Person ein UserControl, das Control hat selbstverständlich ein eigenes ViewModel welches Commands und Events zur Verfügung stellt.
    im MainViewModel fängst du events von den PersonViewModels der UserControls. Die UI braucht es einfach nur anzeigen und die Commands direkt rein binden.
    (Das lässt sich auch mit Mediatoren lösen statt Events)

    public class MainViewModel : ObservableObject
    {
        public MainViewModel()
        {
            Persons = new EnhancedObservableCollection<PersonViewModel>();
            FillPersons(PersonRepository.CreatePersons());
        }
    
        private void FillPersons(IEnumerable<Person> persons)
        {
            foreach (var person in persons)
            {
                var viewModel = new PersonViewModel(person);
                viewModel.CannotBeDoneInPerson += CannotBeDoneInPerson;
                Persons.Add(viewModel);
            }
        }
    
        public EnhancedObservableCollection<PersonViewModel> Persons { get; set; }
    
        private void CannotBeDoneInPerson(Person person)
        {
            // do anything with person
        }
    }
    
    <Window x:Class"...MainView">
        <ItemsControl ItemsSource="{Binding Persons}">
            <ItemsContro.ItemTemplate>
                <DataTemplate>
                    <Controls:PersonView />
                </DataTemplate>
            </ItemsContro.ItemTemplate>
        </ItemsControl>
    </Window>
    
    public class ComponentObject<TComponent> : ObservableObject
    {
        protected TComponent Component { get; private set; }
    
        public ComponentObject(TComponent component)
        {
            Component = component;
        }
    }
    
    public class PersonViewModel : ComponentObject<Person>
    {
        public PersonViewModel(Person model)
            : base(model)
        {
            DoAnythingCommand = new DelegateCommand(p => NotifyCannotBeDoneInPerson());
        }
    
        public delegate void CannotBeDoneInPersonEventHandler(Person person);
        public event CannotBeDoneInPersonEventHandler CannotBeDoneInPerson;
    
        private void NotifyCannotBeDoneInPerson()
        {
            var handler = CannotBeDoneInPerson;
            if (handler != null)
                handler(Component);
        }
    }
    
    <UserControl x:Class"...PersonView">
        <Grid>
            <Button Content="Do Anything" Command="{Binding DoAnythingCommand}" />
        </Grid>
    </UserControl>
    


  • Vielen Dank für die sehr ausführliche Antwort.
    Jetzt muss ich mir das erst mal sehr genau durchlesen. Wahrscheinlich mehrmals bevor ich das alles verstehe und dann auch auf mein bestehendes Projekt übertragen kann.

    Du hast mir auf jeden Fall schon mal sehr viel geholfen. Danke nochmal.



  • Es gibt selbstverständlich verschiedene Lösungswege.
    Ich habe nur zwei aufgeführt die mir als erstes in den Sinn kommen.

    Am Ende geht es darum das man im C# Code nur noch mit den echten Objekten arbeitet statt mit den UI Elementen.
    Dadurch ist es egal ob das Command von einem Button kommt, ein MenuItem, ob es in ein UserControl liegt oder nicht usw. usf.

    Variante 1 hat den Vorteil das man kein PersonViewModel braucht und dadurch weniger Mehrarbeit, hat aber den Nachteil, sobald mehr passieren soll wird die MainViewModel schnell aufgebläht.

    Variante 2 hat den Vorteil das man die Bearbeitung der Personen direkt in das PersonViewModel Objekt auslagern kann, hat aber den Nachteil das man mehr zu tun hat wenn die MainViewModel die Aktionen ausführen soll.

    Muss man von Fall zu Fall einfach schauen was am besten passt und seine Strategie entwickeln 🙂



  • Hallo

    Zu dem was ich bisher hatte passt die Variante 2 ganz gut. Was ich aber noch nicht ganz verstanden habe, wie denn die Bindung zwischen der MainView und dem MainViewModel zustande kommt.

    In Mainview steht ja einfach nur <ItemsControl ItemsSource="{Binding Persons}">
    Woher weiß nun Mainview aus welcher Klasse er Persons verwenden soll und vor allem aus welchem Objekt?



  • Das Binding sucht im Objekt im DataContext. Wenn es da kein Objekt gibt klettert es den Visual Tree hinauf. So lange bisher ein Objekt findet und sucht dann dort nach dem Property namen.

    Das MainViewModel gehört ins DataContext der MainView, dadurch wird das Binding irgendwann darauf stoßen und darin das Property suchen.

    Das kannst du entweder aus der CodeBehind aus machen:

    public partial class MainView : Window
    {
        public MainView()
        {
            InitializeComponent();
    
            DataContext = new MainViewModel();
        }
    }
    

    Manche MVVM Frameworks arbeiten da aber schlauer, da sagt man sowas wie

    var myViewModel = new MainViewModel();
    var windowService = ServiceLocator.Resolve<IWindowService>();
    windowService.ShowDialog(myViewModel);
    

    und der Servcie sucht selbstständig die passende View, erstellt diese und packt das übergebene ViewModel in dessen DataContext.
    So mache ich es nur und biete es auch mit den DW.Services an.

    DataContext & Binding = WPF Basics.
    Fenster aus ViewModels heraus anzeigen = MVVM Basics. Da fehlt dir einfach die Erfahrung wenn du gerade erst mit dem Pattern zu tun bekommst.

    Grundsätzlich:
    Die View kann das ViewModel kennen, aber das ViewModel darf keine Ausgaben kennen (keine Fenster, keine MessageBoxen usw).


Anmelden zum Antworten