在 MVVM 场景中,我应该如何轮询数据库 table 以获得 'live' 视图?

How should I poll a db table for a 'live' view in an MVVM scenario?

我有一个日志数据库 table,我想在 GridView 中显示。如果 table 已更改,我已经有一个视图模型可以轮询数据库并更新整个 ObservableCollection。我对这些数据没有做任何其他事情,所以我认为没有必要使用更深层次的模型。

但是,现在我开始认为 viewmodel 对视图和模型以外的任何东西都起作用是不合适的,我应该引入一个模型来观察数据库并为 viewmodel 提供任何更改。视图模型中更新的 ObservableCollection 然后将新数据传送到视图中的 DataGrid

的确,您的 ViewModel 不应该做任何属于 "the model" 的工作,在本例中是数据访问。

确实有很多方法可以实现这一点,但我会围绕您的数据库相关逻辑实施 repository pattern。然后将 repository 依赖项注入您的 ViewModel.

这两个家伙如何交流是另一回事。您可以不时地使用 Task.Run() 或不阻塞 UI.

的类似方式请求新数据

或者,如果您愿意,可以制作一个单独的 "repository poller",在查询数据时通过 eventspub/sub 和其他方式通知 ViewModel

所以最小的模拟 repository,传递普通的 strings,可能看起来像这样。

// repository 
public interface IRepository<T>
{
    Task<IEnumerable<T>> GetAll();
}

// repository impl
public class SimpleRepository : IRepository<string>
{
    private readonly IList<string> _items = new List<string>();

    public SimpleRepository()
    {}

    public Task<IEnumerable<string>> GetAll()
    {
        if (_items.Count > 10)
            _items.Clear();
        _items.Add(string.Format("string{0}", _items.Count));

        Thread.Sleep(250); // queries take some time...
        return Task.FromResult((IEnumerable<string>) _items);
    }
}

为了举例,只有 GetAll() 方法从存储库返回所有 "records"。

现在您 ViewModel 可以按以下方式使用此存储库。

// ViewModelBase just implements the INotifyPropertyChanged
public class MainViewViewModel : ViewModelBase
{
    private ObservableCollection<string> _items;

    public MainViewViewModel()
        : this(new SimpleRepository())
    {}

    // pass in the repository dependency
    public MainViewViewModel(IRepository<string> simpleRepository)
    {
        SimpleRepository = simpleRepository;
        Task.Run(async () =>
            {
                // sophisticated polling logic here
                while (true)
                {
                    // update collection
                    var results = await SimpleRepository.GetAll();
                    Items = new ObservableCollection<string>(results);
                    Thread.Sleep(250);
                }
            });
    }

    public IRepository<string> SimpleRepository { get; set; }

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

所以这会不时地更新 ObservableCollection<T>,现在 ViewModel 的逻辑已经不存在了。如果您想测试一下,请参阅下面的相关 XAML

<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:MainViewViewModel />
    </Window.DataContext>

    <Grid Margin="10">
        <ItemsControl ItemsSource="{Binding Items}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding}"></TextBlock>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

而且它的外观、感觉和行为确实很专业! :)


如果您也希望 ViewModel 承担投票责任,那么您的 RepositoryPoller 可以显示为

// generic poller
public class RepositoryPoller<T>
{
    public event EventHandler<RepositoryEventArgs<T>> OnQueryComplete;
    private readonly System.Timers.Timer _timer;
    private TimeSpan _timeSpan;

    // wire-up poll timer
    public RepositoryPoller()
    {
        _timer = new System.Timers.Timer();
        _timer.Elapsed += (sender, args) => Query();
    }

    // provide poll interval and repository, or set via properties
    public RepositoryPoller(TimeSpan timeSpan, IRepository<T> repository)
        :this()
    {
        TimeSpan = timeSpan;
        Repository = repository;
    }

    public TimeSpan TimeSpan
    {
        get { return _timeSpan; }
        set { _timeSpan = value; _timer.Interval = _timeSpan.TotalMilliseconds; }
    }

    public IRepository<T> Repository { get; set; }

    public void Start()
    {
        if (TimeSpan.TotalMilliseconds > 0)
            _timer.Start();
    }

    public void Stop()
    {
        _timer.Stop();
    }

    // query for data
    private async void Query()
    {
        var results = (await Repository.GetAll()).ToArray();
        RaiseQueryCompleted(results);
        NotifyQueryCompleted(results);
    }

    // send results as event
    private void RaiseQueryCompleted(IEnumerable<T> results)
    {
        var handler = OnQueryComplete;
        if (handler != null)
            handler(this, new RepositoryEventArgs<T>(results));
    }

    // send results as message
    private void NotifyQueryCompleted(IEnumerable<T> results)
    {
        Messenger.Default.Send(new GenericMessage<IEnumerable<T>>(this, results));
    }
}

// event args holding queried items
public class RepositoryEventArgs<T> : EventArgs
{
    public RepositoryEventArgs(IEnumerable<T> result)
    {
        Results = result;
    }

    public IEnumerable<T> Results { get; set; }
}

所以 poller 查询给定间隔之间的数据,并通过旧 event 和更松散耦合的 message(使用 MVVMLight Libraries NuGet 依赖)通知监听器。

在您的 ViewModel 中,您可以像这样使用它

public MainViewViewModel()
    : this(new RepositoryPoller<string>(
          TimeSpan.FromSeconds(0.5d), new SimpleRepository()))
{}

public MainViewViewModel(RepositoryPoller<string> repositoryPoller)
{
    RepositoryPoller = repositoryPoller;

    // If you prefer pub/sub...
    Messenger.Default.Register<GenericMessage<IEnumerable<string>>>(this, message =>
        {
            var results = message.Content;
            Items = new ObservableCollection<string>(results);
        });

    // Or in case events feel more liek home
    RepositoryPoller.OnQueryComplete += (sender, args) =>
        {
            var results = args.Results;
            Items = new ObservableCollection<string>(results);
        };
    RepositoryPoller.Start();
}

public RepositoryPoller<string> RepositoryPoller { get; set; }

希望它给了你一些想法。