跨平台WPF音乐商店应用程序

avatar
作者
筋斗云
阅读量:0

目录

一 简介

二 设计思路

三 源码


一 简介

支持在线检索音乐,支持实时浏览当前收藏的音乐及音乐数据的持久化。

二 设计思路

采用MVVM架构,前后端分离,子界面弹出始终位于主界面的中心。

三 源码

视窗引导启动源码:

namespace Avalonia.MusicStore {     public class ViewLocator : IDataTemplate     {          public Control? Build(object? data)         {             if (data is null)                 return null;              var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);             var type = Type.GetType(name);              if (type != null)             {                 var control = (Control)Activator.CreateInstance(type)!;                 control.DataContext = data;                 return control;             }              return new TextBlock { Text = "Not Found: " + name };         }          public bool Match(object? data)         {             return data is ViewModelBase;         }     } }        using Avalonia; using Avalonia.ReactiveUI; using System;  namespace Avalonia.MusicStore {     internal sealed class Program     {         // Initialization code. Don't use any Avalonia, third-party APIs or any         // SynchronizationContext-reliant code before AppMain is called: things aren't initialized         // yet and stuff might break.         [STAThread]         public static void Main(string[] args) => BuildAvaloniaApp()             .StartWithClassicDesktopLifetime(args);          // Avalonia configuration, don't remove; also used by visual designer.         public static AppBuilder BuildAvaloniaApp()             => AppBuilder.Configure<App>()                 .UsePlatformDetect()                 .WithInterFont()                 .LogToTrace()                 .UseReactiveUI();     } } 

模型源码:

using iTunesSearch.Library; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks;  namespace Avalonia.MusicStore.Models {      public class Album     {         private static iTunesSearchManager s_SearchManager = new();          public string Artist { get; set; }         public string Title { get; set; }         public string CoverUrl { get; set; }          public Album(string artist, string title, string coverUrl)         {             Artist = artist;             Title = title;             CoverUrl = coverUrl;         }          public static async Task<IEnumerable<Album>> SearchAsync(string searchTerm)         {             var query = await s_SearchManager.GetAlbumsAsync(searchTerm)                 .ConfigureAwait(false);              return query.Albums.Select(x =>                 new Album(x.ArtistName, x.CollectionName,                     x.ArtworkUrl100.Replace("100x100bb", "600x600bb")));         }            private static HttpClient s_httpClient = new();         private string CachePath => $"./Cache/{Artist} - {Title}";          public async Task<Stream> LoadCoverBitmapAsync()         {             if (File.Exists(CachePath + ".bmp"))             {                 return File.OpenRead(CachePath + ".bmp");             }             else             {                 var data = await s_httpClient.GetByteArrayAsync(CoverUrl);                 return new MemoryStream(data);             }         }          public async Task SaveAsync()         {             if (!Directory.Exists("./Cache"))             {                 Directory.CreateDirectory("./Cache");             }              using (var fs = File.OpenWrite(CachePath))             {                 await SaveToStreamAsync(this, fs);             }         }          public Stream SaveCoverBitmapStream()         {             return File.OpenWrite(CachePath + ".bmp");         }          private static async Task SaveToStreamAsync(Album data, Stream stream)         {             await JsonSerializer.SerializeAsync(stream, data).ConfigureAwait(false);         }          public static async Task<Album> LoadFromStream(Stream stream)         {             return (await JsonSerializer.DeserializeAsync<Album>(stream).ConfigureAwait(false))!;         }          public static async Task<IEnumerable<Album>> LoadCachedAsync()         {             if (!Directory.Exists("./Cache"))             {                 Directory.CreateDirectory("./Cache");             }              var results = new List<Album>();              foreach (var file in Directory.EnumerateFiles("./Cache"))             {                 if (!string.IsNullOrWhiteSpace(new DirectoryInfo(file).Extension)) continue;                  await using var fs = File.OpenRead(file);                 results.Add(await Album.LoadFromStream(fs).ConfigureAwait(false));             }              return results;         }     } } 

模型视图源码:

using Avalonia.Media.Imaging; using Avalonia.MusicStore.Models; using ReactiveUI; using System.Threading.Tasks;  namespace Avalonia.MusicStore.ViewModels {     public class AlbumViewModel : ViewModelBase     {         private readonly Album _album;         public AlbumViewModel(Album album)         {             _album = album;         }          public string Artist => _album.Artist;         public string Title => _album.Title;          private Bitmap? _cover;          public Bitmap? Cover         {             get => _cover;             private set => this.RaiseAndSetIfChanged(ref _cover, value);         }          public async Task LoadCover()         {             await using (var imageStream = await _album.LoadCoverBitmapAsync())             {                 Cover = await Task.Run(() => Bitmap.DecodeToWidth(imageStream, 400));             }         }           public async Task SaveToDiskAsync()         {             await _album.SaveAsync();              if (Cover != null)             {                 var bitmap = Cover;                  await Task.Run(() =>                 {                     using (var fs = _album.SaveCoverBitmapStream())                     {                         bitmap.Save(fs);                     }                 });             }         }     } } 
using Avalonia.MusicStore.Models; using ReactiveUI; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Windows.Input;  namespace Avalonia.MusicStore.ViewModels {     public class MainWindowViewModel : ViewModelBase     {         public ICommand BuyMusicCommand { get; }         public Interaction<MusicStoreViewModel, AlbumViewModel?> ShowDialog { get; }         public ObservableCollection<AlbumViewModel> Albums { get; } = new();           public MainWindowViewModel()         {             ShowDialog = new Interaction<MusicStoreViewModel, AlbumViewModel?>();             BuyMusicCommand = ReactiveCommand.CreateFromTask(async () =>             {                 var store = new MusicStoreViewModel();                  var result = await ShowDialog.Handle(store);                  if (result != null)                 {                     Albums.Add(result);                     await result.SaveToDiskAsync();                 }             });             RxApp.MainThreadScheduler.Schedule(LoadAlbums);         }          private async void LoadAlbums()         {             var albums = (await Album.LoadCachedAsync()).Select(x => new AlbumViewModel(x));              foreach (var album in albums)             {                 Albums.Add(album);             }              foreach (var album in Albums.ToList())             {                 await album.LoadCover();             }         }     } } 
using Avalonia.MusicStore.Models; using ReactiveUI; using System; using System.Collections.ObjectModel; using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Threading;  namespace Avalonia.MusicStore.ViewModels {     public class MusicStoreViewModel : ViewModelBase     {         private string? _searchText;         private bool _isBusy;          public string? SearchText         {             get => _searchText;             set => this.RaiseAndSetIfChanged(ref _searchText, value);         }          public bool IsBusy         {             get => _isBusy;             set => this.RaiseAndSetIfChanged(ref _isBusy, value);         }          private AlbumViewModel? _selectedAlbum;         public ObservableCollection<AlbumViewModel> SearchResults { get; } = new();         public AlbumViewModel? SelectedAlbum         {             get => _selectedAlbum;             set => this.RaiseAndSetIfChanged(ref _selectedAlbum, value);         }          public MusicStoreViewModel()         {             this.WhenAnyValue(x => x.SearchText)                 .Throttle(TimeSpan.FromMilliseconds(400))                 .ObserveOn(RxApp.MainThreadScheduler)                 .Subscribe(DoSearch!);              BuyMusicCommand = ReactiveCommand.Create(() =>             {                 return SelectedAlbum;             });         }          private async void DoSearch(string s)         {             IsBusy = true;             SearchResults.Clear();              _cancellationTokenSource?.Cancel();             _cancellationTokenSource = new CancellationTokenSource();             var cancellationToken = _cancellationTokenSource.Token;              if (!string.IsNullOrWhiteSpace(s))             {                 var albums = await Album.SearchAsync(s);                  foreach (var album in albums)                 {                     var vm = new AlbumViewModel(album);                      SearchResults.Add(vm);                 }                  if (!cancellationToken.IsCancellationRequested)                 {                     LoadCovers(cancellationToken);                 }             }              IsBusy = false;         }           private async void LoadCovers(CancellationToken cancellationToken)         {             foreach (var album in SearchResults.ToList())             {                 await album.LoadCover();                  if (cancellationToken.IsCancellationRequested)                 {                     return;                 }             }         }          private CancellationTokenSource? _cancellationTokenSource;         public ReactiveCommand<Unit, AlbumViewModel?> BuyMusicCommand { get; }      } } 
using ReactiveUI;  namespace Avalonia.MusicStore.ViewModels {     public class ViewModelBase : ReactiveObject     {     } } 

视图源码:

<UserControl     x:Class="Avalonia.MusicStore.Views.AlbumView"     xmlns="https://github.com/avaloniaui"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"     xmlns:vm="using:Avalonia.MusicStore.ViewModels"     Width="200"     d:DesignHeight="450"     d:DesignWidth="800"     x:DataType="vm:AlbumViewModel"     mc:Ignorable="d">     <StackPanel Width="200" Spacing="5">         <Border ClipToBounds="True" CornerRadius="10">             <Panel Background="#7FFF22DD">                 <Image                     Width="200"                     Source="{Binding Cover}"                     Stretch="Uniform" />                 <Panel Height="200" IsVisible="{Binding Cover, Converter={x:Static ObjectConverters.IsNull}}">                     <PathIcon                         Width="75"                         Height="75"                         Data="{StaticResource music_regular}" />                 </Panel>             </Panel>         </Border>         <TextBlock HorizontalAlignment="Center" Text="{Binding Title}" />         <TextBlock HorizontalAlignment="Center" Text="{Binding Artist}" />     </StackPanel> </UserControl> 
<Window     x:Class="Avalonia.MusicStore.Views.MainWindow"     xmlns="https://github.com/avaloniaui"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"     xmlns:views="clr-namespace:Avalonia.MusicStore.Views"     xmlns:vm="using:Avalonia.MusicStore.ViewModels"     Title="Avalonia.MusicStore"     d:DesignHeight="450"     d:DesignWidth="800"     x:DataType="vm:MainWindowViewModel"     Background="Transparent"     ExtendClientAreaToDecorationsHint="True"     Icon="/Assets/avalonia-logo.ico"     TransparencyLevelHint="AcrylicBlur"     WindowStartupLocation="CenterScreen"     mc:Ignorable="d">      <Panel>         <ExperimentalAcrylicBorder IsHitTestVisible="False">             <ExperimentalAcrylicBorder.Material>                 <ExperimentalAcrylicMaterial                     BackgroundSource="Digger"                     MaterialOpacity="0.65"                     TintColor="Black"                     TintOpacity="1" />             </ExperimentalAcrylicBorder.Material>         </ExperimentalAcrylicBorder>         <Panel Margin="40">             <Button                 HorizontalAlignment="Right"                 VerticalAlignment="Top"                 Command="{Binding BuyMusicCommand}">                 <PathIcon Data="{StaticResource store_microsoft_regular}" />             </Button>             <ItemsControl Margin="0,40,0,0" ItemsSource="{Binding Albums}">                 <ItemsControl.ItemsPanel>                     <ItemsPanelTemplate>                         <WrapPanel />                     </ItemsPanelTemplate>                 </ItemsControl.ItemsPanel>                  <ItemsControl.ItemTemplate>                     <DataTemplate>                         <views:AlbumView Margin="0,0,20,20" />                     </DataTemplate>                 </ItemsControl.ItemTemplate>             </ItemsControl>         </Panel>     </Panel>  </Window> 
using Avalonia.MusicStore.ViewModels; using Avalonia.ReactiveUI; using ReactiveUI; using System.Threading.Tasks;  namespace Avalonia.MusicStore.Views {     public partial class MainWindow : ReactiveWindow<MainWindowViewModel>     {         public MainWindow()         {             InitializeComponent();             this.WhenActivated(action => action(ViewModel!.ShowDialog.RegisterHandler(DoShowDialogAsync)));         }          private async Task DoShowDialogAsync(InteractionContext<MusicStoreViewModel,                                         AlbumViewModel?> interaction)         {             var dialog = new MusicStoreWindow();             dialog.DataContext = interaction.Input;              var result = await dialog.ShowDialog<AlbumViewModel?>(this);             interaction.SetOutput(result);         }     } }
<UserControl     x:Class="Avalonia.MusicStore.Views.MusicStoreView"     xmlns="https://github.com/avaloniaui"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"     xmlns:vm="using:Avalonia.MusicStore.ViewModels"     d:DesignHeight="450"     d:DesignWidth="800"     x:DataType="vm:MusicStoreViewModel"     mc:Ignorable="d">     <DockPanel>         <StackPanel DockPanel.Dock="Top">             <TextBox Text="{Binding SearchText}" Watermark="Search for Albums...." />             <ProgressBar IsIndeterminate="True" IsVisible="{Binding IsBusy}" />         </StackPanel>         <Button             HorizontalAlignment="Center"             Command="{Binding BuyMusicCommand}"             Content="Buy Album"             DockPanel.Dock="Bottom" />         <ListBox             Margin="0,20"             Background="Transparent"             ItemsSource="{Binding SearchResults}"             SelectedItem="{Binding SelectedAlbum}">             <ListBox.ItemsPanel>                 <ItemsPanelTemplate>                     <WrapPanel />                 </ItemsPanelTemplate>             </ListBox.ItemsPanel>         </ListBox>     </DockPanel> </UserControl> 
<Window     x:Class="Avalonia.MusicStore.Views.MusicStoreWindow"     xmlns="https://github.com/avaloniaui"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"     xmlns:views="using:Avalonia.MusicStore.Views"     Title="MusicStoreWindow"     Width="1000"     Height="550"     ExtendClientAreaToDecorationsHint="True"     TransparencyLevelHint="AcrylicBlur"     WindowStartupLocation="CenterOwner"     mc:Ignorable="d">     <Panel>         <ExperimentalAcrylicBorder IsHitTestVisible="False">             <ExperimentalAcrylicBorder.Material>                 <ExperimentalAcrylicMaterial                     BackgroundSource="Digger"                     MaterialOpacity="0.65"                     TintColor="Black"                     TintOpacity="1" />             </ExperimentalAcrylicBorder.Material>         </ExperimentalAcrylicBorder>          <Panel Margin="40">             <views:MusicStoreView />         </Panel>     </Panel> </Window> 
using Avalonia.MusicStore.ViewModels; using Avalonia.ReactiveUI; using ReactiveUI; using System;  namespace Avalonia.MusicStore.Views {     public partial class MusicStoreWindow : ReactiveWindow<MusicStoreViewModel>     {         public MusicStoreWindow()         {             InitializeComponent();             this.WhenActivated(action => action(ViewModel!.BuyMusicCommand.Subscribe(Close)));         }     } } 

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!