Table<> jako ItemsSource dla ListView- widok się nie odświeża

0

Ustawiam obiekt Table<> rzutowany na IEnumerable<> jako ItemsSource dla ListView. Podczas startu aplikacji poprawnie wczytywana i wyświetlana jest lista z bazy danych. Jednak jeśli dokonam jakichkolwiek zmian (dodawanie, usuwanie) na liście w trakcie działania aplikacji, elementy wyświetlane w ListView nie zmieniają się, pomimo tego, że wszystkie zmiany w Table<> zachodzą poprawnie.

public class ProfilesSourceSQLite : IProfilesSource
{
    public event EventHandler<ProfileActionEventArgs> ActionPerformed;

    private Table<ProfileManager.Profile> Profiles;
    public IEnumerable<ProfileManager.Profile> ProfilesCollection { get { return Profiles; } } 

    public ProfilesSourceSQLite()
    {
        ...
        dbContext = new DataContext(dbConnection);
        Profiles = dbContext.GetTable<ProfileManager.Profile>();
    }

    public bool AddProfile(ProfileManager.Profile profile)
    {
        try
        {
            Profiles.InsertOnSubmit(profile);
            dbContext.SubmitChanges();
        }
        ...

        if(ActionPerformed != null)
            ActionPerformed(this, new ProfileActionEventArgs(ProfileAction.Add, profile));
    }
}

public class ProfileManager
{
    private static ProfileManager instance;
    public static ProfileManager Instance
    {
        get
        {
            if (instance == null)
                instance = new ProfileManager();

            return instance;
        }
    }

    private IProfilesSource profilesSource;
    public IEnumerable<Profile> ProfilesCollection { get { return profilesSource.ProfilesCollection; } }

    public event EventHandler ProfilesListChanged;

    public ProfileManager()
    {
        ...
        profilesSource = new ProfilesSourceSQLite();
        profilesSource.ActionPerformed += OnActionPerformed;
    }

    private void OnActionPerformed(object sender, ProfileActionEventArgs e)
    {
        if(ProfilesListChanged != null)
            ProfilesListChanged(this, new EventArgs());
    }

    public bool AddProfile(Profile profile)
    {
        return profilesSource.AddProfile(profile);
    }
}

public class ProfileManagerWindow : Window
{
    public ProfileManagerWindow()
    {
        InitializeComponent();

        ProfileManager.Instance.ProfilesListChanged += RefreshList;
        listView.ItemsSource = ProfileManager.Instance.ProfilesCollection;
    }

    private void RefreshList(object sender, EventArgs e)
    {
        listView.Items.Refresh();

        // kiedy na liście znajduje się 1 profil, a ja dodaję kolejny:
        int x = ProfileManager.Instance.ProfilesCollection.Count; // x = 2, czyli wartość taka, jaka powinna być
        int y = listView.Items.Count;                                     // y = 1, błędna wartość, zmiana nie została uwzględniona

        // Próbowałem też tak:
        listView.ItemsSource = null;
        listView.ItemsSource = ProfileManager.Instance.ProfilesCollection;
        // ale to również nie działa
    }
}

Co robię źle?

0

Powinieneś bindować listview z ObservableCollection. Link

0

Rozwiązałem problem pisząc taką klasę, której używam zamiast standardowego Table<>. Jest ustawiona jako ItemsSource (w moim kodzie jest jeszcze rzutowana na IEnumerable<>, ale nie wiem czy to ma jakieś znaczenie).

    public sealed class ObservableTable<T> : INotifyCollectionChanged, ITable<T> where T : class
    {
        private Table<T> _table;

        public Expression Expression => _table.AsQueryable().Expression;
        public Type ElementType => _table.AsQueryable().ElementType;
        public IQueryProvider Provider => _table.AsQueryable().Provider;

        public event NotifyCollectionChangedEventHandler CollectionChanged;

        public IEnumerator<T> GetEnumerator()
        {
            return _table.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public ObservableTable(ref DataContext dbContext)
        {
            _table = dbContext.GetTable<T>();

            if(CollectionChanged != null)
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        public void InsertOnSubmit(T entity)
        {
            _table.InsertOnSubmit(entity);

            if (CollectionChanged != null)
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entity));
        }

        public void Attach(T entity)
        {
            _table.Attach(entity);
        }

        public void DeleteOnSubmit(T entity)
        {
            var index = IndexOf(entity);

            _table.DeleteOnSubmit(entity);

            if (CollectionChanged != null)
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, entity, index));
        }

        public int IndexOf(T entity)
        {
            return _table.ToList().IndexOf(entity);
        }
    }

Dodatkowo, jeśli chcemy aby lista odświeżała się również po zmianie wartości w już istniejącym obiekcie na liście, klasa którą przechowujemy musi implementować INotifyPropertyChanged.

0
cnyk napisał(a):

Rozwiązałem problem pisząc taką klasę, której używam zamiast standardowego Table<>. Jest ustawiona jako ItemsSource (w moim kodzie jest jeszcze rzutowana na IEnumerable<>, ale nie wiem czy to ma jakieś znaczenie).
Dodatkowo, jeśli chcemy aby lista odświeżała się również po zmianie wartości w już istniejącym obiekcie na liście, klasa którą przechowujemy musi implementować INotifyPropertyChanged.

A czy implementowałeś INotifyPropertyChanged? I czy to działa? Czy w ogóle istnieje możliwość ustawienia takiego ItemsSource, jeśli dane są pobierane z bazy?

Mam na myśli to, że baza CHYBA i tak będzie za każdym razie pobierała całość, kasowała obecne obiekty i wgrywała nowe do kolekcji, chyba że się mylę?

0

Jeśli chodzi o INotifyPropertyChanged to oczywiście, że działa. Dlaczego miałoby nie działać? Akurat ja użyłem ReSharpera do automatycznej implementacji, ale jest wiele przykładów jak to zrobić: http://stackoverflow.com/questions/1315621/implementing-inotifypropertychanged-does-a-better-way-exist

Table<> implementuje IEnumerable<> czyli dokładnie typ, jakim jest ItemsSource więc znów, dlaczego miałoby nie działać?

U mnie wygląda to tak, że cała tabela jest pobierana tylko raz przez DataContext.GetTable<T>(). Dodawanie rekordu wygląda tak:

var table = new ObservableTable<T>();
...
table.InsertOnSubmit(new T());
dbContext.SubmitChanges();

I na liście od razu pojawia się to co dodałem bez żadnego dodatkowego kodu.

0
cnyk napisał(a):

I na liście od razu pojawia się to co dodałem bez żadnego dodatkowego kodu.

Ciekawa jestem czy dałoby się to coś takiego zrobić w oparciu o TreeView. Aby dodany potomek automatycznie był dodawany bez odświeżania wszystkiego.

0

@quechua: akurat tak się składa, że w swoim projekcie również używam TreeView i właśnie przed chwilą zajmowałem się tą częścią. Więc: tak, z TreeView też da się to zrobić, wszystko automatycznie się dodaje. Pisałem to w oparciu o ten artykuł: http://blog.clauskonrad.net/2011/04/how-to-make-hierarchical-treeview.html
A u mnie wygląda to dokładnie tak (wyświetlane jest drzewko folderów i plików):

        public class Item : INotifyCollectionChanged, INotifyPropertyChanged
        {
            public event NotifyCollectionChangedEventHandler CollectionChanged;
            public event PropertyChangedEventHandler PropertyChanged;

            private string _name;
            private string _path;

            public Item()
            {
                Items = new ObservableCollection<Item>();
                Items.CollectionChanged += delegate(object sender, NotifyCollectionChangedEventArgs action)
                {
                    CollectionChanged?.Invoke(sender, action);
                };
            }

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

            public string Path
            {
                get { return _path; }
                set { _path = value; NotifyPropertyChanged("Path"); }
            }

            public ObservableCollection<Item> Items { get; }

            protected void NotifyPropertyChanged(string property)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
            }
        }

        public class File : Item
        { }

        public class Directory : Item
        { }

XAML:

<Window xmlns:local="clr-namespace:NaszNamespace" ... >

        <Window.DataContext>
            <local:Item />
        </Window.DataContext>

        <TreeView x:Name="treeView">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Items}" DataType="{x:Type local:Item}">
                    <TreeViewItem Header="{Binding Name}" />
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>

</Window>

Użycie:

    public partial class MainWindow : Window
    {      
        private ObservableCollection<Item> _items;

        public MainWindow()
        {
            InitializeComponent();

            _items = new ObservableCollection<Item>();
            _items.Add(new Directory { Name = "folder", Path = "/" });
            _items[0].Items.Add(new File { Name = "plik.jpg", Path = "/folder/" });
            _items.Add(new File { Name = "plik.pdf", Path = "/" });

            treeView.ItemsSource = _items;
        }

        // wszelkie zmiany na _items są teraz od razu widoczne w TreeView bez żadnego dodatkowego kodu
0

Możesz jeszcze pokazać kod, który podczas pracy programu dodaje element (w Twoim przypadku jak rozumiem tworzący folder/plik)?

0

To tylko

_items.Add(new File { Name = "plik.ext", Path = "/" });

Żeby dodać do konkretnej gałęzi wystarczy ją znaleźć i zrobić tak samo

_items[index].Items.Add(new File { Name = "plik.pdf", Path = "/" });

1 użytkowników online, w tym zalogowanych: 0, gości: 1