使用MVVM,ObservableCollection和Reactive Extensions(Rx)进行无效的跨线程访问

时间:2012-07-24 22:56:11

标签: c# .net mvvm system.reactive reactive-programming

我目前正在开发Windows Phone应用程序,我想使用Reactive Extensions来创建异步,以获得更好的UI体验。

我使用MVVM模式:我的View在我的ViewModel中有一个ListBox绑定到ObservableCollection的Items。 Item具有Name或IsSelected等传统属性。

<ListBox SelectionMode="Multiple" ItemsSource="{Binding Checklist, Mode=TwoWay}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

我在App.xaml.cs中实例化我的MainViewModel:

public partial class App : Application
{
    private static MainViewModel _viewModel = null;

    public static MainViewModel ViewModel
    {
        get
        {
            if (_viewModel == null)
                _viewModel = new MainViewModel();

            return _viewModel;
        }
    }

    [...]
}

我将数据加载到MainPage_Loaded。

public partial class MainPage : PhoneApplicationPage
{
   public MainPage()
    {
        InitializeComponent();
        this.DataContext = App.ViewModel;
        this.Loaded += new System.Windows.RoutedEventHandler(MainPage_Loaded);
    }

    private void MainPage_Loaded(object sender, System.Windows.RoutedEventArgs e)
    {
        App.ViewModel.LoadData();
    }
}

我从我的ViewModel加载我的数据(我稍后会将该代码移到模型中):

public class MainViewModel : ViewModelBase
{
    public ObservableCollection<Item> _checklist;

    public ObservableCollection<Item> Checklist
    {
        get
        {
            return this._checklist;
        }
        set
        {
            if (this._checklist != value)
            {
                this._checklist = value;
            }
        }
    }

    private const string _connectionString = @"isostore:/ItemDB.sdf";

    public void LoadData()
    {
        using (ItemDataContext context = new ItemDataContext(_connectionString))
        {
            if (!context.DatabaseExists())
            {
                // Create database if it doesn't exist
                context.CreateDatabase();
            }

            if (context.Items.Count() == 0)
            {
                [Read my data in a XML file for example]

                // Save changes to the database
                context.SubmitChanges();
            }

            var contextItems = from i in context.Items
                                select i;

            foreach (Item it in contextItems)
            {
                this.Checklist.Add(it);
            }
        }
    }

    [...]
}

它工作正常,项目在视图中更新。

现在我想创建异步。使用传统的BeginInvoke是一种新方法,我从View而不是LoadData调用它可以正常工作。

public partial class MainPage : PhoneApplicationPage
{
    [...]

    private void MainPage_Loaded(object sender, System.Windows.RoutedEventArgs e)
    {
        App.ViewModel.GetData();
    }
}

我使用一个名为CurrentDispatcher的属性,它在App.xaml.cs中用App.Current.RootVisual.Dispatcher填充。

public class MainViewModel : ViewModelBase
{
    [...]

    public Dispatcher CurrentDispatcher { get; set; }

    public void GetData()
    {
        this.CurrentDispatcher.BeginInvoke(new Action(LoadData));
    }

    [...]
}

但我想使用Reactive Extentions。所以我尝试了像ToAsync或ToObservable这样的Rx的不同元素,但是当我将一个项目添加到清单时,我有一些“UnauthorizedAccessException未处理”和“无效的跨线程访问”。

我试过ObserveOn其他线程导致错误来自UI和后台线程之间的混合,但它不起作用。也许我不像我在那种特殊情况下那样使用Rx?

任何帮助都会非常感激。

在你的回答后编辑:

这是一个效果很好的代码!

public void GetData()
{
    Observable.Start(() => LoadData())
    .ObserveOnDispatcher()
    .Subscribe(list =>
    {
        foreach (Item it in list)
        {
            this.Checklist.Add(it);
        }
    });
}

public ObservableCollection<Item> LoadData()
{
    var results = new ObservableCollection<Item>();

    using (ItemDataContext context = new ItemDataContext(_connectionString))
    {
        //Loading

        var contextItems = from i in context.Items
                           select i;

        foreach (Item it in contextItems)
        {
            results.Add(it);
        }
    }

    return results;
}

如你所见,之前没有正确使用过。现在我可以使用ObservableCollection并在Susbscribe中使用它。这是完美的!非常感谢!

2 个答案:

答案 0 :(得分:3)

您的代码中似乎没有异步性(保存BeginInvoke,我认为它不会按照您的想法进行)。

我假设您要使用Rx,因为您的代码目前全部阻塞,并且在建立与数据库的连接并加载数据时,UI没有响应: - (

你想要做的是在后台线程上执行“繁重的工作”,然后一旦你有值,只需将它们添加到Dispatcher上的ObservableCollection。您可以通过以下三种方式之一完成此任务:

  1. 一次性归还整个系列。然后循环遍历列表,使用foreach循环添加到ObservableCollection。如果列表太大,这可能会阻止UI(无响应的应用程序)
  2. 一次返回一个值集合,在对调度程序的独立调用中将每个项目添加到ObservableCollection。这将使UI保持响应,但可能需要更长时间才能完成
  3. 以缓冲块的形式返回集合并尝试充分利用这两个世界
  4. 您想要的代码可能看起来像这样

    public IList<Item> FetchData()
    {
      using (ItemDataContext context = new ItemDataContext(_connectionString))
      {
        //....
        var results = new List<Item>();
        foreach (Item it in contextItems)
        {
          results.Add(it);
        }
        return results;
      }
    }
    public void LoadData()
    {
      Observable.Start(()=>FetchData())
                .ObserveOnDispatcher()
                .Subscribe(list=>
                  {
                    foreach (Item it in contextItems)
                    {
                      this.Checklist.Add(it);
                    }
                  });
    }
    

    代码不适合单元测试,但看起来你不感兴趣(静态成员,带有DB连接的VM等等),所以这可能会起作用吗?!

答案 1 :(得分:0)

我自己就碰到了这个。有多种方法可以到达调度员,我必须使用以下内容。我直接从我的ViewModels中调用它(不从外部传入)。

Application.Current.Dispatcher.Invoke(action);