目录
一 简介
二 设计思路
三 源码
一 简介
支持在线检索音乐,支持实时浏览当前收藏的音乐及音乐数据的持久化。



二 设计思路
采用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)));
        }
    }
}



















