阅读量: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))); } } }