在 mvvm 问题中创建 class 结构

creating class structure in mvvm issues

我有以下 Classes:
项目

public class Item : INotifyPropertyChanged, IDataErrorInfo
{
    private int? id;
    public int? ID
    {
        get
        { return id; }
        set
        { id = value; }
    }

    private string name;
    public string Name
    {
        get
        { return name; }
        set
        {
            if (value != name)
            {
                ClearError("Name");
                if (string.IsNullOrEmpty(value) || value.Trim() == "")
                    SetError("Name", "Required Value");
                name = value;
            }
        }
    }
    private List<MedicineComposition> medicineCompositions;
    public List<MedicineComposition> MedicineCompositions
    {
        set { medicineCompositions = value; }
        get { return medicineCompositions; }
    }
}

药物成分

public class MedicineComposition : INotifyPropertyChanged, IDataErrorInfo
{
    private int? id;
    public int? ID
    {
        get
        { return id; }
        set
        { id = value; }
    }

    private Item item;
    public Item Item
    {
        get
        { return item; }
        set
        {
            if (item != value)
            {
                ClearError("Item");
                if (value == null)
                    SetError("Item", "Required Value");
                item = value;
            }
        }
    }
    private Component component;
    public Component Component
    {
        get
        { return component; }
        set
        {
            if (component != value)
            {
                ClearError("Component");
                if (value == null)
                    SetError("Component", "Required Value");
                component = value;
            }
        }
    }
}

Component 只有 idName
以及以下从数据库中获取数据并列出我的对象的函数: GetItemsItem Class

public static List<Item> GetAllItems
{
get
{
    List<Item> MyItems = new List<Item>();
    SqlConnection con = new SqlConnection(BaseDataBase.ConnectionString);
    SqlCommand com = new SqlCommand("sp_Get_All_Item", con);
    com.CommandType = System.Data.CommandType.StoredProcedure;
    try
    {
        con.Open();
        SqlDataReader rd = com.ExecuteReader();
        while (rd.Read())
        {
            Item i = new Item();
            if (!(rd["ID"] is DBNull))
                i.ID = System.Int32.Parse(rd["ID"].ToString());
            i.Name = rd["Name"].ToString();
            i.MedicineCompositions = MedicineComposition.GetAllByItem(i);

            MyItems.Add(i);
        }
        rd.Close();
    }
    catch
    {
        MyItems = null;
    }
    finally
    {
        con.Close();
    }
    return MyItems;
}

GetAllByItemMedicalCompositions

public static List<MedicineComposition> GetAllByItem(Item i)
{
    List<MedicineComposition> MyMedicineCompositions = new List<MedicineComposition>();

    SqlConnection con = new SqlConnection(BaseDataBase.ConnectionString);
    SqlCommand com = new SqlCommand("sp_Get_ByItemID_MedicineComposition", con);
    com.CommandType = System.Data.CommandType.StoredProcedure;
    SqlParameter pr = new SqlParameter("@ID", i.ID);
    com.Parameters.Add(pr);
    try
    {
        con.Open();
        SqlDataReader rd = com.ExecuteReader();
        while (rd.Read())
        {
            MedicineComposition m = new MedicineComposition() { };
            if (!(rd["ID"] is DBNull))
                m.ID = Int32.Parse(rd["ID"].ToString());
            if (!(rd["ComponentID"] is DBNull))
                m.Component = Component.GetByID(Int32.Parse(rd["ComponentID"].ToString()));
            m.Item = i;
            MyMedicineCompositions.Add(m);
        }
        rd.Close();
    }
    catch
    {
        MyMedicineCompositions = null;
    }
    finally
    {
        con.Close();
    }
    return MyMedicineCompositions;
}

它喜欢使用 mvvm 因为它可以让你处理对象而不是 datatable,但是当我使用以前的 class 结构时我有 以下问题:

我认为您的用户不需要一眼就看到所有 1000 个项目,甚至不需要相关的数千个组成和组件。

遇到这种情况我会:

  1. 过滤数据。向用户询问项目名称、类别或其他内容。
  2. 延迟加载。首先只加载(过滤的)项目。当用户 select 一个 Item 切换到一个 "Item details" 查看并加载相关数据(组成和组件)时。

将数据集分配给 ObservableCollection 属性 的构造函数。否则,您的视图将通过 PropertyChanged 通知更新您的 ObservableCollection 执行添加操作的每个项目。

试试这个:

var items = services.LoadItems();
myObservableCollection = new ObservableCollection<somedatatype>(items);

这种类型的赋值将通知您的视图一次,而不是您实施的当前方式,即 1000 次。

而不是 returning 列表,return IEnumerable 并根据需要生成结果。显然,当您没有阅读所有结果时,它只会提高性能,这在大多数情况下确实如此。为此,您必须删除 catch,因为您不能将 yield 和 catch 放在一起。 catch 可以绕过 con.Open 和 ExecuteReader 并且在 catch 中你可以 yield break:

        public static IEnumerable<MedicineComposition> GetAllByItem(Item i)
    {
        SqlConnection con = new SqlConnection(BaseDataBase.ConnectionString);
        SqlCommand com = new SqlCommand("sp_Get_ByItemID_MedicineComposition", con);
        com.CommandType = System.Data.CommandType.StoredProcedure;
        SqlParameter pr = new SqlParameter("@ID", i.ID);
        com.Parameters.Add(pr);
        try
        {
            SqlDataReader rd;
            try
            {
                con.Open();
                rd = com.ExecuteReader();
            }
            catch { yield break;}
            while (rd.Read())
            {
                MedicineComposition m = new MedicineComposition() { };
                if (!(rd["ID"] is DBNull))
                    m.ID = Int32.Parse(rd["ID"].ToString());
                if (!(rd["ComponentID"] is DBNull))
                    m.Component = Component.GetByID(Int32.Parse(rd["ComponentID"].ToString()));
                m.Item = i;
                yield return m;
            }
            rd.Close();
        }
        finally
        {
            con.Close();
        }
    } 

现在,如果出现异常,这不再是 returning null,而是 return 一些项目甚至是空枚举。我宁愿将 catch 移至此 getter 的调用者。 如果出于某种原因需要计算 returned 项,请调用 GetAllByItem(item).ToArray()。这将枚举所有项目一次并为您获取长度。绝对不要调用枚举两次获取长度再枚举项:

var length = GetAllByItem(item).Count();// this will get all the items from the db
foreach(var i in GetAllByItem(item)) // this will get all the items from the db again

最好这样做:

var list = GetAllByItem(item); // this will get all the items and now you have the length and the items.

显然,如果您出于某种原因需要长度,则更改为 IEnumerable 没有意义,只是为了更好的抽象。

其他改进可能是,只创建一次连接而不是每次调用 getter。只有当你知道它不会造成任何伤害时,才有可能。

您可以在此处进行一些改进,例如:

  • 考虑到我们正在谈论 MedicalComposition,拥有 nullable 个唯一标识符可能不是最好的主意
  • 如果您有多个仅包含 idname 的 class,您可以使用 KeyValuePair<>Tuple<> 代替
  • 实现基础 class,比如 ModelBase 实现 INotifyPropertyChanged
  • 为数据库相关操作实施 repository pattern,如果可能,cache/page 结果
  • 如果尚未完成,请将数据 - and/or 时间密集型操作移动到单独的线程中
  • 有点令人困惑的是,在 Item 上您有 MedicineComposition 的 IEnumerable,但在 MedicineComposition 中您也有 Item?也许您根本不需要它或相关 Item.Id 就足够了?
  • 您可以将一种方法添加到您的存储库中,仅用于 return 自 <timestamp> 以来 added/modified/removed 的项目,并且仅更新 Items 集合中的必要内容
  • 您可以制作一些属性Lazy<>
  • 利用TAP(基于任务的异步模式)

下面是 "one go" 针对您的 w/o 阻塞 UI 线程的问题。它远未完成,但仍然如此。 Thread.Sleep存储库中的数据正在模仿您的数据库查询延迟

View\MainWindow.xaml

代码隐藏仅包含 InitializeComponents

<Window x:Class="WpfApplication1.View.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:viewModel="clr-namespace:WpfApplication1.ViewModel"
        Title="MainWindow"
        Height="300"
        Width="250">
    <Window.DataContext>
        <viewModel:MainViewModel />
    </Window.DataContext>

    <!-- Layout root -->
    <Grid x:Name="ContentPanel" Margin="12,0,12,0">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!-- Status label -->        
        <Label Grid.Row="0"
               HorizontalAlignment="Stretch"
               VerticalAlignment="Top"
               Background="Bisque"
               Margin="0,3,0,3"
               Content="{Binding Status}" />

        <!-- Controls -->        
        <StackPanel Grid.Row="1">
            <Label Content="Items" />
            <!-- Items combo -->
            <ComboBox HorizontalAlignment="Stretch"
                  MaxDropDownHeight="120"
                  VerticalAlignment="Top"
                  Width="Auto" 
                  Margin="0,0,0,5"
                  ItemsSource="{Binding Items}"
                  SelectedItem="{Binding SelectedItem}"
                  DisplayMemberPath="Name" />

            <!-- Medicine components -->
            <ItemsControl ItemsSource="{Binding SelectedItem.MedicineCompositions}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <StackPanel>
                            <TextBlock Text="{Binding Name}" />
                            <!-- Components -->
                            <ItemsControl ItemsSource="{Binding Components}">
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate>
                                        <TextBlock>
                                            <Run Text=" * " />
                                            <Run Text="{Binding Name}" />
                                        </TextBlock>
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>
                        </StackPanel>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </StackPanel>
    </Grid>
</Window>

ViewModel\MainViewModel

public class MainViewModel : ViewModelBase
{
    private string _status;
    private Item _selectedItem;
    private ObservableCollection<Item> _items;

    public MainViewModel()
        :this(new ItemRepository(), new MedicineCompositionRepository())
    {}

    public MainViewModel(IRepository<Item> itemRepository, IRepository<MedicineComposition> medicineCompositionRepository)
    {
        ItemRepository = itemRepository;
        MedicineCompositionRepository = medicineCompositionRepository;
        Task.Run(() => LoadItemsData());
    }

    public IRepository<Item> ItemRepository { get; set; }

    public IRepository<MedicineComposition> MedicineCompositionRepository { get; set; }

    public Item SelectedItem
    {
        get { return _selectedItem; }
        set
        {
            _selectedItem = value; 
            OnPropertyChanged();
            Task.Run(() => LoadMedicineCompositionsData(_selectedItem));
        }
    }

    public ObservableCollection<Item> Items
    {
        get { return _items; }
        set { _items = value; OnPropertyChanged(); }
    }

    public string Status
    {
        get { return _status; }
        set { _status = value; OnPropertyChanged(); }
    }

    private async Task LoadItemsData()
    {
        Status = "Loading items...";

        var result = await ItemRepository.GetAll();
        Items = new ObservableCollection<Item>(result);

        Status = "Idle";
    }

    private async Task LoadMedicineCompositionsData(Item item)
    {
        if (item.MedicineCompositions != null)
            return;

        Status = string.Format("Loading compositions for {0}...", item.Name);

        var result = await MedicineCompositionRepository.GetById(item.Id);
        SelectedItem.MedicineCompositions = result;

        Status = "Idle";
    }
}

型号

public class Component : ModelBase
{}

public class MedicineComposition : ModelBase
{
    private IEnumerable<Component> _component;

    public IEnumerable<Component> Components
    {
        get { return _component; }
        set { _component = value; OnPropertyChanged(); }
    }
}

public class Item : ModelBase
{
    private IEnumerable<MedicineComposition> _medicineCompositions;

    public IEnumerable<MedicineComposition> MedicineCompositions
    {
        get { return _medicineCompositions; }
        set { _medicineCompositions = value; OnPropertyChanged(); }
    }
}

public abstract class ModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private int _id;
    private string _name;

    public int Id
    {
        get { return _id; }
        set { _id = value; OnPropertyChanged(); }
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; OnPropertyChanged(); }
    }

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

存储库

public interface IRepository<T> where T : class
{
    Task<IEnumerable<T>> GetAll();
    Task<IEnumerable<T>> GetById(int id);
}

public class ItemRepository : IRepository<Item>
{
    private readonly IList<Item> _mockItems; 

    public ItemRepository()
    {
        _mockItems = new List<Item>();
        for (int i = 0; i < 100; i++)
            _mockItems.Add(new Item { Id = i, Name = string.Format("Item #{0}", i), MedicineCompositions = null });

    }

    public Task<IEnumerable<Item>> GetAll()
    {
        Thread.Sleep(1500);
        return Task.FromResult((IEnumerable<Item>) _mockItems);
    }

    public Task<IEnumerable<Item>> GetById(int id)
    {
        throw new NotImplementedException();
    }
}

public class MedicineCompositionRepository : IRepository<MedicineComposition>
{
    private readonly Random _random;

    public MedicineCompositionRepository()
    {
         _random = new Random();
    }

    public Task<IEnumerable<MedicineComposition>> GetAll()
    {
        throw new NotImplementedException();
    }

    public Task<IEnumerable<MedicineComposition>> GetById(int id)
    {
        // since we are mocking, id is actually ignored
        var compositions = new List<MedicineComposition>();

        int compositionsCount = _random.Next(1, 3);
        for (int i = 0; i <= compositionsCount; i++)
        {
            var components = new List<Component>();

            int componentsCount = _random.Next(1, 3);
            for (int j = 0; j <= componentsCount; j++)
                components.Add(new Component {Id = j, Name = string.Format("Component #1{0}", j)});
            compositions.Add(new MedicineComposition { Id = i, Name = string.Format("MedicalComposition #{0}", i), Components = components });
        }

        Thread.Sleep(500);
        return Task.FromResult((IEnumerable<MedicineComposition>) compositions);
    }
}