Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

Schorsch

Supermoderator

  • »Schorsch« ist der Autor dieses Themas

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

1

11.01.2014, 18:09

WPF Slider Binding MVVM

Ich arbeite zur Zeit an einer etwas größeren WPF Anwendung und merke dabei dass ich doch so einiges noch nicht wirklich richtig anwenden kann.
Mein Szenario ist folgendes:
Meine View hat das ViewModel als DataContext gesetzt.
In der View befindet sich ein Slider.
Das Model hat 2 Properties vom Typ long welche die Werte des Sliders bestimmen sollen. Im Endeffekt handelt es sich um einen Musikplayer und der Slider soll die aktuelle Spielzeit des Songs anzeigen und manipulieren können. Die beiden Properties setzen also Maximum und Position des Sliders. Ein Timer sorgt dafür dass in einem bestimmten Intervall die Positionsproperty neu gesetzt wird.
Jetzt müssen diese Daten irgendwie an die View geliefert werden. Weiterhin soll es möglich sein den Slider mit der Maus zu manipulieren und so die Position des Songs zu ändern. Mein aktueller Ansatz funktioniert zwar, dabei baue ich mir aber einen Kreislauf und dadurch wird das ganze langsam und ruckelig. Aufwendig und unschön ist das ganze auch. Die Länge des Songs, also die zweite Property lasse ich erst mal außen vor. Hier geht das Binding nur in eine Richtung. Das läuft auch soweit. So nun zu meinem bisherigen Vorgehen:
In der View binde ich die Position des Sliders an eine DependencyProperty des ViewModels. Das Model implementiert INotifyPropertyChangend. In einem Zeitintervall wird die aktuelle Position im Song geholt und die Property im Model dafür aktualisiert. Sobald die Property geändert wird wird das Event gefeuert. Das ViewModel empfängt das Event und passt die DependencyProperty an.
Bis jetzt klappt noch alles. Ist zwar etwas aufwendig, aber es klappt. Nun geht es um die andere Richtung. Ich möchte den Slider verschieben und so die Position des Songs manipulieren. Dafür bekommt die DependencyProperty Metadaten zugewiesen. Dort setze ich ein PropertyChangedCallback. Sobald die DependencyProperty geändert wird rufe ich auf dem Model eine Funktion auf die die aktuelle Position im Song selbst verschiebt.
Hier kommt es nun zum Problem. In beide Richtungen beeinflusse ich die DependencyProperty und dadurch wird diese Callback Funktion aufgerufen. Jetzt kommt es zum Kreislauf.
XAML Code für den Slider in meiner View:

Quellcode

1
<Slider DockPanel.Dock="Top" Value="{Binding CurrentTime}" Maximum="{Binding CurrentChannelLength}" />


ViewModel auf das wichtigste reduziert:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class PlayerViewModel:DependencyObject
    {
        //...

        private AudioPlayer player;
        public AudioPlayer Player { get { return player; } }

        public PlayerViewModel()
        {
            player = new AudioPlayer();
            player.PropertyChanged += player_PropertyChanged;

            //...
        }

        void player_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName.Equals("CurrentChannelPosition"))
            {
                CurrentTime = player.CurrentChannelPosition;
            }
            else if(e.PropertyName.Equals("CurrentChannelLength"))
            {
                CurrentChannelLength = player.CurrentChannelLength;
            }
        }

        public static readonly DependencyProperty CurrentTimeProperty = DependencyProperty.Register("CurrentTime",
            typeof(long), typeof(PlayerViewModel),
            new PropertyMetadata(setCurrentTime));

        private static void setCurrentTime(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            (o as PlayerViewModel).player.SetChannelPosition((long)e.NewValue);
        }

        public long CurrentTime
        {
            get { return (long)GetValue(CurrentTimeProperty); }
            set { SetValue(CurrentTimeProperty, value); }
        }

        public static readonly DependencyProperty CurrentChannelLengthProperty = DependencyProperty.Register("CurrentChannelLength", typeof(long), typeof(PlayerViewModel));
        public long CurrentChannelLength
        {
            get { return (long)GetValue(CurrentChannelLengthProperty); }
            set { SetValue(CurrentChannelLengthProperty, value); }
        }

        //...
    }


Model auf das wesentliche reduziert:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class AudioPlayer:INotifyPropertyChanged
    {

        //....

        private DispatcherTimer positionTimer;
        private long currentChannelPosition;
        private long currentChannelLength;
        public long CurrentChannelPosition { get { return currentChannelPosition; } set { currentChannelPosition = value; NotifyPropertyChanged("CurrentChannelPosition"); } }
        public long CurrentChannelLength { get { return currentChannelLength; } set { currentChannelLength = value; NotifyPropertyChanged("CurrentChannelLength"); } }

        //...

        private int currentStreamHandle;
        public int CurrentStreamHandle
        {
            get { return currentStreamHandle; }
            set { currentStreamHandle = value; }
        }

        public AudioPlayer()
        {
            positionTimer = new DispatcherTimer(DispatcherPriority.ApplicationIdle);
            positionTimer.Interval = TimeSpan.FromMilliseconds(100);
            positionTimer.Tick += positionTimerTick;
            positionTimer.IsEnabled = true;

           //...
        }

        //...

        public void SetChannelPosition(long position)
        {
            CurrentChannelPosition = Math.Max(0, Math.Min(position, CurrentChannelLength));
            Bass.BASS_ChannelSetPosition(CurrentStreamHandle, Math.Max(0, Math.Min(position, CurrentChannelLength)));
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
    }


Beim laden des Songs wird die Länge des Songs festgelegt. Wie gesagt die Lösung ist alles andere als sauber und auch nicht wirklich gut. An sich sollte das doch eigentlich ein recht normales Problem sein. Deshalb hoffe ich, dass ihr mich auf den richtigen Weg leiten könnt;)
Schönen Abend noch.
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

Sylence

Community-Fossil

Beiträge: 1 663

Beruf: Softwareentwickler

  • Private Nachricht senden

2

11.01.2014, 19:12

Warum hast du in deinem ViewModel überhaupt eine Property für die Länge und Position des Songs?
Ich würde dem ViewModel einfach eine Property "CurrentSong" geben und dann in der View an "CurrentSong.CurrentTime" und "CurrentSong.CurrentChannelLength" binden.

Sacaldur

Community-Fossil

Beiträge: 2 301

Wohnort: Berlin

Beruf: FIAE

  • Private Nachricht senden

3

11.01.2014, 19:33

Bei einer Webanwendung, für die ich einen Videoplayer (HTML5 wurde im Hintergrund verwendet) umgesetzt habe, hatte ich genau das gleiche Problem.
In meinem Fall ließ es sich so lösen, dass dem Slider der Wert auf eine Weise gegeben werden konnte, der nicht für ein Auslösen des entsprechenden Events sorgte.

In deinem Fall fällt mir auf Anhieb die Möglichkeit ein, dass du prüfst, ob die Änderung mindestens einem bestimmten Wert (bspw. 0,5 Sekunden) oder mehr entspricht und andernfalls keine Änderung vornimmst.
Alternativ könntest du gucken, ob du evtl. 2 Methoden zum Setzen der aktuellen Position implementierst (vergleichbar dem Vorgehen bei mir).
Oder, was auch fast schon wieder sauber ist, aber nur fast, du könntest dieses Binding auf eine einzige Richtung (der Slider liest lediglich) beschränken und über ein entsprechendes Event/Command, welches bei einer Wertänderung ausgelöst wird, die Steuerung des Players übernehmen. (Das setzt voraus, dass dieses Event entweder nicht bei einer Änderung über das Binding sondern nur über einen Userinput ausgeführt wird oder dass in diesem der Ursprung, Binding oder Benutzer, erkannt werden kann.

(Es kann sein, dass die eine oder andere Möglichkeit sich evtl. nicht umsetzen lässt, gefühltermaßen fehlt mir in deinem Code noch der Überblick.)

Und noch so am Rande: Solltest du dir Commands noch nicht angesehen haben, würde ich empfehlen, das nachzuholen. (Ich sehe in deinem Code lediglich ein Callback für ein Event. Es kann sein, dass er an dieser Stelle der Einfachheit halber verwendet wird und ansonsten Commands, vielleicht ist es aber auch nicht so...)


@Sylence:
Ich denke mal, dass dadurch das Problem nur verschoben wird.
Abgesehen davon kann ich deiner Variante nur bedingt zustimmen: Wie lang ein Song ist, ist die Eigenschaft eines Songs, wie weit er bereits abgespielt wurde aber nicht. Gäbe es mehrere Player nebeneinander, dann könnte der gleiche Song im einem pausiert sein und im anderen weiterlaufen (auch wenn das wenig sinnvoll klingt: man nehme einfach Soundeffekte in einem Spiel, bei denen einzelne ggf. mehrmals "gleichzeitig" abgespielt werden könnten.)
Die Länge des Songs kann also durchaus verlagert werden (und der aktuelle Song aufgenommen werden, sollte er noch nicht da sein).
Spieleentwickler in Berlin? (Thema in diesem Forum)
---
Es ist ja keine Schande etwas falsch zu machen, als Programmierer tu ich das täglich, [...].

4

11.01.2014, 20:29

Sollte es nicht reichen vor dem Schmeißen des Events zu prüfen, ob sich der zu setzende Wert von dem aktuellen unterscheidet?

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

5

11.01.2014, 21:18

Ich würde das ganz anders lösen. Ich würde die Dependency Property wegwerfen und eine einfache Property binden. Wird diese von außen gesetzt, wird die Position im Musikstück geändert. Den Getter derselben Property würde ich die aktuelle Position im Track zurückgeben lassen.
Soll sich nur die Position des Sliders an die aktuelle Zeit anpassen, würde ich dem Model genau das sagen, sodass es nur eine Notification wirft und das Binding die Property neu abfragt - womit ja dann wieder die aktuelle Zeit ausgelesen wird.
Quasi in etwa so:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
public long CurrentPosition
{
  get { return CurrentSong.Position; }
  set { Bass....setPosition(value); NotifyPropertyChanged("CurrentPosition"); }
}

public ForceVisualUpdate()
{
  NotifyPropertyChanged("CurrentPosition");
}

Das Binding des Sliders hängt an ersterem, der Timer ruft letzteres.
Teamleiter von Rickety Racquet (ehemals das "Foren-Projekt") und von Marble Theory

Willkommen auf SPPRO, auch dir wird man zu Unity oder zur Unreal-Engine raten, ganz bestimmt.[/Sarkasmus]

Schorsch

Supermoderator

  • »Schorsch« ist der Autor dieses Themas

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

6

12.01.2014, 13:02

Vielen Dank für die vielen Antworten:)
@Silence:
Ich habe es so verstanden, als dass das ViewModel die Daten bereitstellt (als Properties), welche die View anzeigen soll. Diese Daten müssen nicht immer 100% mit Model übereinstimmen. Ein Beispiel wäre eine Liste welche sich filtern lässt. Bei diesen beiden Properties ist es ja eher unwahrscheinlich, dass diese mit dem Model nicht übereinstimmen, aber ich habe das so als Konvention verstanden und fand es auch nicht ganz unsinnig. Ansonsten wäre die ViewModel Schicht relativ unnötig. Lässt sich aber sicherlich drüber streiten.
@Sacaldur:
Ich habe das meiste aus dem Code geworfen bevor ich es hier gepostet habe. Im Prinzip ist nur das was ich gepostet habe für das Problem interessant und ich wollte es ein wenig übersichtlicher halten. Ich kenne das ja selbst wenn man so einen Batzen Code dahin geknallt kriegt. Da verliert man zu schnell den Überblick. Was Command angeht, so findet man davon natürlich auch einige in meinem Code. Ich wüsste jetzt nur nicht an welcher Stelle ich hier welche benutzen sollte/könnte. Für den Slider macht es meiner Meinung nach hier weniger Sinn. Vielleicht verstehe ich dich da aber auch grad nur falsch.
Die Möglichkeit zu gucken wie extrem sich der Wert ändert und dann zu entscheiden funktioniert nur bedingt. Erst mal muss ich einen eher willkürlichen Wert finden und alles unterhalb dieses Wertes kann dann nicht verschoben werden. Möglich, aber nicht wirklich schön.
Die Variante mit den 2 Methoden hatte ich ja schon versucht, das Problem im Code waren aber die DependencyProperties und deren Aktualisierung. Dadurch hatte ich mir einen Kreislauf gebaut.
@Chromanoid:
Geändert hat sich der Wert immer. Nur ein mal ging es aus der Audio Engine im Model und einmal aus der View heraus.

BlueCobolds Hinweis hat perfekt geholfen. In der Richtung hatte ich angefangen. Hab mir dann irgendwann meinen Kreisel da eingebaut und stand auf dem Schlauch. Vielen Dank für die Hilfe.
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

7

13.01.2014, 15:34

@Chromanoid:
Geändert hat sich der Wert immer. Nur ein mal ging es aus der Audio Engine im Model und einmal aus der View heraus.

Ich hatte es so verstanden, dass es zu einem rekursiven Aufruf kommt. Ein Vergleich der Werte hätte die Rekursion dann unterbrochen.

Quellcode

1
2
public long CurrentChannelPosition { get { return currentChannelPosition; } set { long old = currentChannelPosition; currentChannelPosition = value; if(old!=value) NotifyPropertyChanged("CurrentChannelPosition"); } }
public long CurrentChannelLength { get { return currentChannelLength; } set { long old = currentChannelLength; currentChannelLength = value; if(old!=value) NotifyPropertyChanged("CurrentChannelLength"); } }

Werbeanzeige