预览效果

分类列表

分类列表

编辑弹窗

1685342541-image

1. CategoryPage

1.1 创建分类页面 GridView+ScrollViewer

页面布局采用Grid+StackPanel。Grid用于区分顶部添加按钮与下方分类列表,在列表内容超出屏幕时滚动不会影响顶部操作按钮。

     <Grid x:Name="ContentArea">
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <RelativePanel Grid.Row="0">
            <Button x:Uid="Category_Create" Click="CategoryCreate_Click"></Button>
        </RelativePanel>
        <ScrollViewer Grid.Row="1" 
                      Name="ForegroundElement"
                      HorizontalAlignment="Stretch"
                      VerticalScrollMode="Enabled"
                      IsTabStop="True">
            <StackPanel  Style="{ThemeResource StackCardPanelStyle}" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
                <GridView x:Name="CategoryList"
                      Margin="{StaticResource SmallTopMargin}"
                      ItemTemplate="{StaticResource CategoryGridViewTemplate}"
                      ItemsSource="{x:Bind ViewModel.CategoryList,Mode=OneWay}"
                      BorderThickness="0"
                      BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
                      MinWidth="400"
                      HorizontalAlignment="Left"/>
            </StackPanel>
        </ScrollViewer>
    </Grid>

1.2 GridView自定义展示效果

通过自定义ItemTemplate来实现GridView的展示效果,通过Grid布局实现上方显示图片,下方显示分类名称,并用InfoBadge来实现分类颜色的展示,注意设置宽高属性和Magrin。由于分类图片我统一从emoji下载的高清图240*240,这里图片就采用的80*80的尺寸。

        <DataTemplate x:Key="CategoryGridViewTemplate" x:DataType="model:CategoryDto">
            <Grid Margin="{StaticResource SmallLeftRightMargin}" ContextRequested="CategoryList_ContextRequested">
                <Grid.RowDefinitions>
                    <RowDefinition Height="90"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="16"/>
                    <ColumnDefinition Width="100"/>
                </Grid.ColumnDefinitions>
                <Image Stretch="Fill" Height="80" Width="80" Source="{x:Bind Icon}" Grid.ColumnSpan="2" Margin="5"/>
                <InfoBadge x:Name="infoBadge2" Style="{StaticResource AttentionIconInfoBadgeStyle}" Grid.Row="1" Background="{x:Bind Color}" />
                <TextBlock Grid.Row="1" Grid.Column="1"
                           Text="{x:Bind Name}"
                           x:Phase="1"
                           HorizontalAlignment="Center"
                           HorizontalTextAlignment="Center"
                           Margin="{StaticResource SmallTopBottomMargin}"/>
            </Grid>
        </DataTemplate>

1.3 分类右键点击菜单 CommandBarFlyout

1685344098-image

按钮的文字绑定多语言资源,通过x:Uid绑定,如AppBarButton中显示文本为Label,对应在Resources.resw中配置key为Button_Edit.Label。其他类似,如xxx.Content、xxx.Text。

右键菜单通过ContextRequested绑定到想要触发的元素上。

         <CommandBarFlyout Placement="Right" x:Name="CategoryCommandBarFlayout">
            <CommandBarFlyout.SecondaryCommands>
                <AppBarButton x:Uid="Button_Edit" Icon="Edit" Click="AppBarButton_Edit_Click" />
                <AppBarButton x:Uid="Button_Delete" Icon="Delete" Click="AppBarButton_Delete_Click" />
            </CommandBarFlyout.SecondaryCommands>
        </CommandBarFlyout>

后台方法实现

// 延迟关闭右键菜单
private void DelayCloseFlyout()
{
    Task.Delay(10).ContinueWith(_ => CategoryCommandBarFlayout.Hide(), TaskScheduler.FromCurrentSynchronizationContext());
}
// 显示右键菜单
private void ShowMenu(bool isTransient, UIElement element)
{
    FlyoutShowOptions myOption = new FlyoutShowOptions();
    myOption.ShowMode = isTransient ? FlyoutShowMode.Transient : FlyoutShowMode.Standard;
    CategoryCommandBarFlayout.ShowAt(element, myOption);
}

2. CategoryViewModel

新建SelectedCatgeory,用于绑定分类列表选中的对象,方便后续删改更新页面。分类列表使用ObservableCollection,可以实时更新页面。

注:数据请求更新使用EFCore.Sqlite

 
public class CategoryViewModel : ObservableRecipient
{
    private readonly IMapper _mapper;
 
    public CategoryDto SelectedCategory
    {
        get; set;
    }
 
    public ObservableCollection<CategoryDto> CategoryList
    {
        get; private set;
    }
 
    public CategoryViewModel()
    {
        _mapper = App.GetService<IMapper>();
        SelectedCategory = new();
        CategoryList = new();
        ListCategories();
    }
 
    public void ListCategories()
    {
        CategoryList.Clear();
        using var context = new ApplicationDbContext();
        var list = _mapper.Map<List<CategoryDto>>(context.CategoryModels.ToList());
        foreach (var item in list)
        {
            CategoryList.Add(item);
        }
    }
    // 修改后重载页面数据,实现无刷新更新
    public void ListReload(CategoryModel newCategory)
    {
        var newDto = _mapper.Map<CategoryDto>(newCategory);
        if (newCategory.ID > 0)
        {
            CategoryList.Remove(SelectedCategory);
        }
        CategoryList.Add(newDto);
    }
 
    public void AddCategory(CategoryModel category)
    {
        using var context = new ApplicationDbContext();
        context.Add(category);
        context.SaveChanges();
    }
 
    public void UpdateCategory(CategoryModel category)
    {
        using var context = new ApplicationDbContext();
        context.Update(category);
        context.SaveChanges();
    }
 
    public void DeleteCategory(int id)
    {
        using var context = new ApplicationDbContext();
        var deleteModel = context.Find<CategoryModel>(id);
        if (deleteModel != null)
        {
            context.CategoryModels.Remove(deleteModel);
            context.SaveChanges();
        }
    }
    public CategoryModel? FindCategory(int id)
    {
        using var context = new ApplicationDbContext();
        var model = context.Find<CategoryModel>(id);
        return model;
    }
}

3. 弹窗编辑功能实现

参考WinUI 3 Gallery实现,自建一个CategoryModifyDialog页面,绑定到ContentDialog的Content中,通过发送当前选中的Category来传递数据。

    private async void OpenModifyModal(CategoryModel? category)
    {
        var dialog = new ContentDialog
        {
            // XamlRoot must be set in the case of a ContentDialog running in a Desktop app
            XamlRoot = this.XamlRoot,
            Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style,
            Title = (category == null || category.ID == 0) ? "新增" : "编辑",
            PrimaryButtonText = "Save",
            CloseButtonText = "Cancel",
            DefaultButton = ContentDialogButton.Primary,
            Content = new CategoryModifyDialog(category)
        };
        dialog.PrimaryButtonClick += Dialog_PrimaryButtonClick;
 
        var result = await dialog.ShowAsync();
    }
 
    private void Dialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
    {
        var model = ((CategoryModifyDialog)sender.Content).CurrentCategory;
        if (model.ID > 0)
        {
            ViewModel.UpdateCategory(model);
        }
        else
        {
            ViewModel.AddCategory(model);
        }
        ViewModel.ListReload(model);
 
    }

4. 弹窗页面实现

<Page.Resources>
        <x:Double x:Key="SwatchSize">32</x:Double>
    </Page.Resources>
    <StackPanel VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
        <!-- Content body -->
        <RelativePanel>
            <SplitButton x:Name="colorButton">
                <Border x:Name="CurrentColor" Width="26" Height="24" Background="{x:Bind CurrentCategory.Color,Mode=TwoWay}" CornerRadius="5"/>
                <SplitButton.Flyout>
                    <Flyout Placement="Bottom">
                        <GridView ItemClick="GridView_ItemClick" IsItemClickEnabled="True">
                            <GridView.ItemsPanel>
                                <ItemsPanelTemplate>
                                    <ItemsWrapGrid MaximumRowsOrColumns="3" Orientation="Horizontal"/>
                                </ItemsPanelTemplate>
                            </GridView.ItemsPanel>
                            <GridView.Resources>
                                <Style TargetType="Rectangle">
                                    <Setter Property="Width" Value="{StaticResource SwatchSize}"/>
                                    <Setter Property="Height" Value="{StaticResource SwatchSize}"/>
                                    <Setter Property="RadiusX" Value="4"/>
                                    <Setter Property="RadiusY" Value="4"/>
                                </Style>
                            </GridView.Resources>
                            <GridView.Items>
                                <Rectangle Fill="BlueViolet"/>
                                <Rectangle Fill="Violet"/>
                                <Rectangle Fill="Red"/>
                                <Rectangle Fill="OrangeRed"/>
                                <Rectangle Fill="Orange"/>
                                <Rectangle Fill="Yellow"/>
                                <Rectangle Fill="DarkCyan"/>
                                <Rectangle Fill="ForestGreen"/>
                                <Rectangle Fill="Gray"/>
                            </GridView.Items>
                        </GridView>
                    </Flyout>
                </SplitButton.Flyout>
            </SplitButton>
            <TextBox Header="" Text="{x:Bind CurrentCategory.Name,Mode=TwoWay}" PlaceholderText="Enter Category name" Width="300" Margin="{StaticResource SmallLeftRightMargin}" RelativePanel.RightOf="colorButton" />
        </RelativePanel>
 
        <TextBlock Text="CategoryIcon" Margin="{StaticResource SmallTopBottomMargin}"></TextBlock>
        <GridView ItemClick="CategoryIcon_ItemClick" SelectedValue="{x:Bind CurrentCategory.IconFile}"  x:Name="CategoryIcon" IsItemClickEnabled="True">
            <GridView.ItemsPanel>
                <ItemsPanelTemplate>
                    <ItemsWrapGrid MaximumRowsOrColumns="5" Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </GridView.ItemsPanel>
            <GridView.ItemTemplate>
                <DataTemplate x:DataType="models:CategoryIconItem">
                    <Image Source="{x:Bind IconFile}" Width="64" Margin="5"></Image>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>
    </StackPanel>

如果新增时,需要给Color设置默认值,防止报错。

    public CategoryModifyDialog(CategoryModel model)
    {
        this.InitializeComponent();
        LoadCategoryIconView();
        if (model == null || model.ID == 0)
        {
            CurrentCategory = new CategoryModel
            {
                Name = "",
                Color = "#95a5a6",
                IconFile = ""
            };
        }
        else
        {
            CurrentCategory = model;
        }
    }

完整代码见https://gitee.com/KnifeZ/win-time