Windows 8 App Libraries laden

Meckie

Ensign
Registriert
Aug. 2005
Beiträge
252
Hallo zusammen ich beschäftige mich gerade ein wenig mit der App Entwicklung unter anderem für Windows 8. So jetzt wollte ich die Daten aus den Libraries laden. Bspw. aus der MusicLibrary.

Nach ein wenig rumsuchen bin ich auf verschiedene Lösungen gestoßen. Hier sind so die 2 die ich ganz gut finde.

1. Mit ObservableCollection konnte man schön die Daten gruppieren und wunderbar anzeigen. Das Problem bei viel Musik ist das einfach zu langsam

2. GetVirtualizedFoldersVector() von der FileInformationFactory. Das funktioniert echt sehr schnell. Habe es auch jetzt hinbekommen, das ich schnell an die MusicProperties komme.

An sich ist der 2. Weg der richtige für viele Daten. Nur wie kann ich die jetzt bspw. in einer GridView sortieren, gruppieren? Ich komme bei diesem Ansatz nicht an die Collection. Und selbst wenn wüsste ich nicht wie ich das handhaben soll. Beim 1. Ansatz hat man die Daten ja bevor man sie der CollectionViewSource zuweist vorbereitet. Das geht ja beim 2. Ansatz nicht.

Auf der folgenden Seite werden beide Ansätze erklärt.
http://msdn.microsoft.com/en-us/library/windows/apps/xaml/hh758320.aspx

Nur wenn es ums grouping geht dann wird immer nur der 1. Ansatz genannt. Ich versteh es nicht und weiß nicht mehr weiter. Bei der Windows Music App funktioniert es ja auch. Zumindest haben die in der ListView auch die Elemente gruppiert. Oder ka wie die das da gemacht haben.

Mfg
Meckie
 
Also, du hast dir da nicht grade das trivialste Problem raus gesucht ;)

Erstmal ein paar Dinge vorweg (du scheinst nicht genau zu wissen, was das eigentliche Problem ist).

Eine ObservableCollection ist eine Collection, die die beiden Interfaces INotifyCollectionChanged und INotifyPropertyChanged implementiert. Benutzen tut man die in der Regel, wenn du die Collection als DataSource (DataBinding) benutzen möchtest (~20x höhere Performance gegenüber einer normalen Collection und halt Benachrichtigungen, wenn sich etwas ändert).

Der Call zu GetVirtualizedFoldersVector bzw. GetVirtualizedFilesVector gibt dir X zurück, wobei X auf jeden Fall IObservableVector<object> implementiert, d.h. du kannst das dahin auch casten. Die einzelnen Items in der Collection werden in ein Objekt vom Typ object "geboxt", tatsächlich handelt es sich um FileInformation bzw. FolderInformation Objekte (abhängig davon, was du abrufst).

Der Unterschied zwischen beiden ist nun, dass ein IObservableVector die Daten virtualisiert (vollständige Virtualisierung, nicht nur UI wie ISupportIncrementalLoading) und eine ObservableCollection nicht, d.h. für letzteres müssen alle Items vorliegen. Bei dem Vektor hingegen gibt der erste Call des Indexers in der Regel null zurück und benachrichtigt die UI, sobald das angeforderte Item geladen wurde (und entlädt die Items auch wieder, sobald sie in der UI nicht mehr sichtbar sind).

Das Problem ist nun (deswegen funktionieren deine Gruppen nicht), bindest du eine IObservableVector Collection an ein GridView, erkennt das GridView dies und ordnet nicht den kompletten Inhalt an, sondern nur das, was derzeit sichtbar ist +- ein paar weitere Items.

Es ist nicht unmöglich das Problem zu lösen, aber es ist ein Haufen arbeit. Hinzu kommt, möchtest du so Gruppieren, wie z.B. im Store, fährst du besser, wenn du gleich HTML5 und JS nimmst anstatt C#. Das GridView in C# deaktiviert "by design" Virtualisierung sobald du Gruppen verwendest (bzw. lässt dich erst gar kein VirtualizingPanel verwenden), d.h. da darfst du dich dann auch noch drum kümmern. Der Grund steht oben, laut Microsoft wird diese Funktionalität aber nachgereicht.

Wie auch immer, was ist dein Plan bzgl. der Gruppen? So wie im Store oder so wie z.B. die Kontakte-App? Kann dir evtl. helfen (eigentlich wollte ich schon vor Wochen über genau dieses Thema einen Artikel in meinem Blog schreiben, komme aber imho zu nix ;))
 
Hi danke für die schnelle Antwort. Mir ist schon aufgefallen das viele Apps in JS geschrieben sind. Also vermutlich erst abwarten. Ist ganz gut hab momentan auch wenig Zeit.

Ich dachte die Gruppen so wie im Kontakte App. Momentan hab ich noch kein Ziel überlege mir noch was ich programmieren möchte. Hab da ein paar Ideen, aber ich wollte erstmal gucken wie ich einige Probleme lösen kann. Hatte bisher meistens mit Java programmiert und wollte mir mal was neues angucken. Da mir scheint das C# vergleichbar ist dachte ich ok warum nicht. Viele Apps gibt es ja noch nicht im Windows Store.

Naja nochmal zum Thema. Aber mit einer ObservableCollection macht es ja keinen sinn. Hab mal den Speicherverbrauch verglichen. fast 300 MB im vergleich zu 70-80MB beim Vector Ansatz.

Cool wäre es wenn ich wüsste wie die Daten gruppiert und sortiert werden können.

So könnte das bspw. aussehen http://s1.directupload.net/file/d/3000/qsazeeje_jpg.htm

Grüße
Meckie


edit: Mich würde jetzt noch interessieren wie du die Properties bspw. MusicProperties lädst. Ich habe das bisher so gemacht und in der xaml beim binding einen Converter anzugeben. In dem habe ich dann die infos geladen. Das muss aber async passieren, da MusicProperties am Anfang wie mir scheint immer leer sind. Geht das noch anders? Hoffe das war verständlich.
 
Zuletzt bearbeitet:
Hi, schonmal sorry für die späte Antwort.

Zum Speicherverbrauch von ObservableVector und ObservableCollection. Wie gesagt, letzteres ist nicht virtualisiert und daher muss jedes Item bereits im Speicher vorliegen. Für den ObservableVector hingegen werden die Items "on-demand" geladen und entladen.

Zum Thema Gruppieren und Sortieren:
Sortieren geht über das SortOrder Property der QueryOptions. Gruppieren kannst du über den ersten ctor Parameter der QueryOptions und es dann, bei Bedarf, über die SearchFilter (ApplicationSearchFilter, UserSearchFilter) verfeinern.

Bzgl. der MusicProperties.
Wenn du die Bulk-Access API benutzt (IObservableVector), werden die Properties direkt mitgeladen, d.h. der Converter ist überflüssig. Bei einer regulären Suche musst du die Properties selber laden. Für Letzteres empfiehlt es sich Property-Prefetching zu benutzen. I.d.R. werden die MusicProperties dadurch fast "instant" geladen. Allerdings kommst du um den "async call" nicht herum. Hinzu kommt, dass trotz Prefetching die Properties manchmal erst geladen werden müssen - zumindest hab ich das selber schon beobachtet.
Wie auch immer, das ganze muss in einem Converter geschehen und das heißt, die musst auf den "async call" warten (await geht nicht, wegen der Interface Definition). Das ganze geschieht im UI Thread und blockt daher die UI - meiner Meinung nach sollte man solche Situationen vermeiden.

So, nun der dicke Batzen - Gruppieren in der UI ;)
Da du die Daten virtualisieren möchtest, ist das etwas komplexer. Dem Screenshot nach, möchtest du nicht wie in der Kontakte-App gruppieren (leider, den das ist ziemlich simpel).
Um das so hinzukriegen (mit C#) kannst du folgendes machen:

  1. Eine reguläre Suche ausführen und schonmal gruppieren, z.B. nach System.AlbumArtist. Kein PropertyPrefetch benutzen, aber auf jeden Fall den Windows Search Indexer mit einbeziehen (die Libraries werden indeziert, User die das manuell ausschalten würde ich eiskalt ignorieren)
  2. Da du bereits gruppiert hast, kannst du das DisplayName Property (z.B. Album-Titel, wenn du nach System.AlbumArtist gruppiertst) verwenden, um nun die eigentlichen Gruppen zu erhalten (Anfangsbuchstaben). Wie du mit Sonderzeichen umgehst weiß ich nicht, evtl. ist String-Normalisierung nötig.
  3. Wenn du nun alle Anfangsbuchstaben hast, führst du für jeden wieder einen Query aus, aber diesmal über die Bulk-Access API. ApplicationSearchFilter eignet sich hierzu.

Bis hier hast du nur die Daten. Das ganze dauert Zeit. Ich komme auf ca. 600-800ms bei einer 4GB Musik-Bibliothek und einer regulären HDD. Am Besten alles in einer Hintergrund-Task laufen lassen.

Der "Tricky-Part" ist nun das ganze auch anzuzeigen. Im ersten Post hab ich es ja schon geschrieben, Die Funktionalität ist imo nicht da, um Virtualisierung mit Gruppen zu benutzen, von daher musst du dir eine Alternative einfallen lassen. Hier ist meine ;)

Ein paar Worte vorweg. Diese Lösung ist nicht optimal. Es wurde zwar Die Gruppen voll virtualisiert, aber nicht die einzelnen Items. D.h. hast du eine Gruppe mit 1000 Items, werden alle 1000 Items geladen. Sollte das bei dir der Fall sein, rate ich dir dazu nicht mit C# zu arbeiten. Schau dir den Quelltext der XBox Music App an. Zu der Performance der App verlier ich aber mal besser kein Wort ;)
Der Grund, das alle Items geladen werden ist, dass du für das zweite Grid Scrolling deaktivieren musst, um keinen doppelten Scroll-Balken zu bekommen. Dadurch wird die Virtualisierung deaktiviert und der Inhalt komplett geladen.

Also, was du brauchst. Hier ein kurzer Anriss der UI

- GridView - VirtualizingStackPanel
- GridView - WrapGrid​
- ItemTemplate​


Das erste GridView hält die Gruppen, welche du in dem zweiten GridView anzeigen kannst.
Dieser Ansatz bringt jetzt ein paar Probleme mit sich.

  1. Für das erste GridView musst du den ItemContainerStyle komplett überschreiben, da sonst eine Gruppe als einzelnes Item behandelt wird. Das ist nicht schwer, aber mühsam.
  2. Das zweite GridView wird alle Maus-Klicks und Scroll-Infos schlucken und nicht an das erste Grid weitergeben, d.h. du musst Scrolling selber implementieren (klingt schlimm, ist einfach). Dies tritt auf, auch wenn du Scrolling explizit deaktivierst (was du tun musst). Microsoft hat schon angekündigt dies demnächst "zu fixen". Touch-Input ist davon nicht betroffen.

Zu 1.:
Wichtig ist, dass du SelectionMode.None verwendest und alle Touch- und Klick-Transistionen entfernst.
Noch ein Tipp: Verwende auf jeden Fall eine MinWidth für die einzelnen Gruppen-Items. Andernfalls werden alle mit 0 initiiert und dadurch alle Gruppen einmal geladen. Du weißt wo groß deine Gruppen sind, du musst das Wissen nur benutzen ;)

Zu 2.:
Hier einfach nur der Code. Es gibt noch eine zweite Möglichkeit in der man einen extra EventHandler vom Zweiten in das ersten Grid registriert. Hatte damit aber immer wieder sehr merkwürdiges Scrollverhalten beobachtet.

Der Code muss in die Code-Behind Datei der entsprechenden Seite:
Code:
private ScrollViewer _scrollViewer;

private void OnWheelChanged( CoreWindow sender, PointerEventArgs args )
{
	if ( _scrollViewer == null )
	{
		_scrollViewer = contentGrid.FindChild<ScrollViewer>( "ScrollViewer" );
	}
	
	if ( _scrollViewer == null )
	{
		return;
	}
	 
	double offset = _scrollViewer.HorizontalOffset < 2 ? 2 : 0.2;
	if ( args.CurrentPoint.Properties.MouseWheelDelta == -120)
	{
		_scrollViewer.ScrollToHorizontalOffset( _scrollViewer.HorizontalOffset + offset );
	}

	if ( args.CurrentPoint.Properties.MouseWheelDelta == 120 )
	{
		_scrollViewer.ScrollToHorizontalOffset( _scrollViewer.HorizontalOffset - offset );
	}

	args.Handled = true;
}

protected override void OnNavigatedTo( Windows.UI.Xaml.Navigation.NavigationEventArgs e )
{
	base.OnNavigatedTo( e );
	Window.Current.CoreWindow.PointerWheelChanged += OnWheelChanged;
}

protected override void OnNavigatedFrom( Windows.UI.Xaml.Navigation.NavigationEventArgs e )
{
	Window.Current.CoreWindow.PointerWheelChanged -= OnWheelChanged;
	base.OnNavigatedFrom( e );
}

Zum scrollen verwende ich einen festen Wert von 0.2. Du kannst den Offset auch berechnen, aber das Ergebnis taumelt irgendwo zwischen 0.18 und 0.22.

Versuch dich am Besten erstmal daran, die Gruppen bei Bedarf in das 1. Grid zu laden. Abhängig davon wie groß deine Gruppen sind, sollte nie mehr als 2-3 Gruppen gleichzeitig geladen sein.
Weiterhin ist es ratsam die Daten über die Musik-Bibliothek zu cachen. Den entsprechenden Query kann man als String speichern und wieder laden. Ändert sich die Collection wird ein Event gefeuert. So musst du auf jeden Fall nicht immer wieder alle Daten laden.
Da es sein kann, dass du länger als eine Sekunde auf die Daten warten musst, ist eine ProgressRing keine schlechte Idee.
 
Zuletzt bearbeitet:
Nochmal zu den Properties. Ich habe das in meinem Spielwiese Projekt so:

Code:
var queryOptions = new QueryOptions(CommonFolderQuery.GroupByAlbum);
queryOptions.FolderDepth = FolderDepth.Deep;
queryOptions.IndexerOption = IndexerOption.UseIndexerWhenAvailable;
queryOptions.SortOrder.Clear();


var sortEntry = new SortEntry();
sortEntry.PropertyName = "System.Music.Artist";
sortEntry.AscendingOrder = true;
queryOptions.SortOrder.Clear();
queryOptions.SortOrder.Add(sortEntry);

List<string> propertyNames = new List<string>();

//Prefetch Properties to acess them fast
propertyNames.Add("System.Music.Artist");
propertyNames.Add("System.Music.AlbumTitle");

queryOptions.SetPropertyPrefetch(PropertyPrefetchOptions.MusicProperties, propertyNames);

var fileQuery = KnownFolders.MusicLibrary.CreateItemQueryWithOptions(queryOptions);

const uint size = 190; // default size for PicturesView mode
var fileInformationFactory = new FileInformationFactory(fileQuery, ThumbnailMode.MusicView, size, ThumbnailOptions.UseCurrentScale, true);

itemsViewSource.Source = fileInformationFactory.GetVirtualizedFilesVector();

Toll ist das er es definitiv nicht nach Artist sortiert. Er macht da zwar was aber es ist nicht nach Artist. Hinzu kommt, dass die MusicProperties alle leer sind bis auf Album. erst mit async komme ich an diese:
Code:
properties.RetrievePropertiesAsync(new[] { "System.Music.AlbumTitle" })

Oder meinst du das mit reguläre Suche. Bin mir da nicht sicher was du genau damit meinst.

Das ist vielleicht auch der Grund warum er mir das nicht richtig sortiert oder ich weiß auch nicht hab nicht so viel Zeit momentan da nochmal genau nachzugucken.

Dein Ansatz klingt interessant. Werde ich mal ausprobieren. Wenn eine Gruppe immer komplett geladen wird ist das glaube für mich nicht so gut. Bspw. wenn ich erlauben möchte, dass er alle Lieder anzeigt, dann sind das recht viele Elemente in einer Gruppe.

Danke für die Antwort
Meckie
 
Du gruppierst nach System.Music.AlbumTitle, willst aber nach Artist sortieren.
Das geht so nicht. Versuch mal nach System.Music.AlbumArtist zu sortieren oder anders zu Gruppieren.

Bzgl. der Properties. Kannst du mal bitte den vollständigen Code posten? Wenn du die Bulk-Access API benutzt, sollten die definitiv nicht leer sein.

Code:
var fileInformationFactory = new FileInformationFactory(fileQuery, ThumbnailMode.MusicView, size, ThumbnailOptions.UseCurrentScale, true);

Oben ist Bulk-Access, unten das was ich mit normaler Suche meine ;)

Code:
var query = KnownFolders.MusicLibrary.CreateFileQueryWithOptions( options );
var files = await query.GetFilesAsync();
 
Hi.
Naja ich habe es nach Artist versucht zu sortieren, da das AlbumTitle Feld immer leer ist.
Genauer:

Ich wollte am Anfang erst einfach nur eine View mit den Cover des Albums und dessen Title.

Wenn ich in XAML direkt als Binding MusicProperties.AlbumTitle angegeben habe hat er da keinen Text angezeigt.

Also habe ich einen Converter gebastelt. Binding ist dann MusicProperties und der Converter sieht so aus.

Code:
        public object Convert(object value, Type targetType, object parameter, string culture)
        {
            MusicProperties properties = (MusicProperties)value;
            var awaiter = properties.RetrievePropertiesAsync(new[] { "System.Music.AlbumTitle" }).GetAwaiter();

            

            while (!awaiter.IsCompleted)
            {
                Task.Delay(1);
            }

            var dict = awaiter.GetResult();
            if (dict.ContainsKey("System.Music.AlbumTitle"))
            {
                return dict["System.Music.AlbumTitle"] as string;
            }

            return "n/a";
        }

Das ging dann. Also er hat dann auch den title angezeigt. Das siehst du ja auf dem Screenshot das ich hochgeladen habe. Den converter hab ich so im inet gefunden. Das mit dem GetAwaiter() ist damit die Methode nicht async sein muss. Soweit ich das verstanden habe.

Dann habe ich geguckt welche Werte ich noch habe und hab festgestellt, dass Artist vorhanden ist aber AlbumArtist nicht. (Kann auch an meinem ID3Tag liegen).
Naja dann dachte ich ok probierst du mal aus nach Artist zu sortieren. Aber er macht das nicht. Ich glaube ja das es wieder daran liegt, dass MusicProperties.Artist am Anfang leer ist, wenn man nicht vorher diesen async Zugriff macht.

Deswegen habe ich das mit dem Prefetch ausprobiert. Das hat aber nur den async Zugriff schneller gemacht aber leider ist MusicProperties.Artist etc. immer noch leer.


Hier sieht man auch ganz gut das die Felder leer sind:
http://social.msdn.microsoft.com/Forums/en-US/winappswithcsharp/thread/dce2738c-93a8-4b50-8ba1-6cf3acaee735

Weiter unten ist ein Link zu seiner source. Da ist sein AlbumArtistSample.

Im Prinzip habe ich es genau so nur das ich es noch mit dem converter ausprobiert habe.
 
Zuletzt bearbeitet:
Der Code kam mir doch gleich bekannt vor :) Den Beitrag den du da aus dem MSDN zitierst, das bin ich ;)

Wie auch immer, in dem Beitrag geht es um System.Music.AlbumArtist, nicht um AlbumTitle. Was du suchst ist MusicProperties.Album, nicht MusicProperties.AlbumTitle - mich würde es allerdings nicht wundern, wenn beide Felder bei dir leer sind. Das Framework scheint etwas "buggy" zu sein. Mit dem Album-Titel hatte ich zumindest nie Probleme.
Das Problem mit dem AlbumArtist Feld habe ich bis heute nicht richtig lösen können. Zum App-Release ist der Converter auf jeden Fall keine Option. (Mein Projekt liegt daher auf Eis - so macht's keinen Sinn)

Ich wäre dir auch sehr dankbar, wenn du den Thread im MSDN pushen könntest. Hab es selber schonmal versucht, aber der Typ vom Media SDK scheint verschollen zu sein ;(
Am besten direkt mit kurzer Beschreibung und Beispiel-Projekt, sonst kommt wieder irgend ein Mod mit der üblichen Hinhaltetaktik von wegen "Mach doch erstmal ein Projekt fertig"...

Edit.
Der Code stammt übrigens aus der RC Version. Mit der RTM habe ich es bisher noch nicht versucht. Arbeitest du mit der RC oder RTM?

Edit2.
Wenn du das Projekt von mir nimmst (MSDN), kriegst du da einen Titel angezeigt?
(Falls du auf RTM bist, einfach im AppManifest die Version von 6.2.0 auf 6.2.1 ändern, dann sollte es gehen.)
 
Zuletzt bearbeitet:
Windows 8 Pro hab ich von MSDNA. Bei Visual Studio Express 2012 steht Version 11.0.50727.1 RTMREL.
Meinst du das? Die Titel zeigt er an ja. Aber den Rest nicht. Naja gucke mir das nach dem 11 nochmal an. Hab am 11.09 eine Prüfung. Toller Termin nebenbei...

edit: oben hab ich mich verschrieben. AlbumTitle ist da. AlbumArtist nicht natürlich.
 
Zuletzt bearbeitet:
Zurück
Oben