4

This is inspired from the following issue Rendering a generated table with TableLayoutPanel taking too long to finish. There are other SO posts regarding WPF tabular data, but I don't think they cover this case (although How to display real tabular data with WPF? is closer). The issue is interesting because both rows and columns are dynamic, and the view should not only display the data initially, but also react on add/remove (both rows and columns) and updates. I'll present the WF way (because I have experience there) and would like to see and compare it to the WPF way(s).

First, here is the sample model to be used in both cases:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
namespace Models
{
    abstract class Entity
    {
        public readonly int Id;
        protected Entity(int id) { Id = id; }
    }
    class EntitySet<T> : IReadOnlyCollection<T> where T : Entity
    {
        Dictionary<int, T> items = new Dictionary<int, T>();
        public int Count { get { return items.Count; } }
        public IEnumerator<T> GetEnumerator() { return items.Values.GetEnumerator(); }
        IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
        public void Add(T item) { items.Add(item.Id, item); }
        public bool Remove(int id) { return items.Remove(id); }
    }
    class Player : Entity
    {
        public string Name;
        public Player(int id) : base(id) { }
    }
    class Game : Entity
    {
        public string Name;
        public Game(int id) : base(id) { }
    }
    class ScoreBoard
    {
        EntitySet<Player> players = new EntitySet<Player>();
        EntitySet<Game> games = new EntitySet<Game>();
        Dictionary<int, Dictionary<int, int>> gameScores = new Dictionary<int, Dictionary<int, int>>();
        public ScoreBoard() { Load(); }
        public IReadOnlyCollection<Player> Players { get { return players; } }
        public IReadOnlyCollection<Game> Games { get { return games; } }
        public int GetScore(Player player, Game game)
        {
            Dictionary<int, int> playerScores;
            int score;
            return gameScores.TryGetValue(game.Id, out playerScores) && playerScores.TryGetValue(player.Id, out score) ? score : 0;
        }
        public event EventHandler<ScoreBoardChangeEventArgs> Changed;
        #region Test
        private void Load()
        {
            for (int i = 0; i < 20; i++) AddNewPlayer();
            for (int i = 0; i < 10; i++) AddNewGame();
            foreach (var game in games)
                foreach (var player in players)
                    if (RandomBool()) SetScore(player, game, random.Next(1000));
        }
        public void StartUpdate()
        {
            var syncContext = SynchronizationContext.Current;
            var updateThread = new Thread(() =>
            {
                while (true) { Thread.Sleep(100); Update(syncContext); }
            });
            updateThread.IsBackground = true;
            updateThread.Start();
        }
        private void Update(SynchronizationContext syncContext)
        {
            var addedPlayers = new List<Player>();
            var removedPlayers = new List<Player>();
            var addedGames = new List<Game>();
            var removedGames = new List<Game>();
            var changedScores = new List<ScoreKey>();
            // Removes
            if (RandomBool())
                foreach (var player in players)
                    if (RandomBool()) { removedPlayers.Add(player); if (removedPlayers.Count == 10) break; }
            if (RandomBool())
                foreach (var game in games)
                    if (RandomBool()) { removedGames.Add(game); if (removedGames.Count == 5) break; }
            foreach (var game in removedGames)
                games.Remove(game.Id);
            foreach (var player in removedPlayers)
            {
                players.Remove(player.Id);
                foreach (var item in gameScores)
                    item.Value.Remove(player.Id);
            }
            // Updates
            foreach (var game in games)
            {
                foreach (var player in players)
                {
                    if (!RandomBool()) continue;
                    int oldScore = GetScore(player, game);
                    int newScore = Math.Min(oldScore + random.Next(100), 1000000);
                    if (oldScore == newScore) continue;
                    SetScore(player, game, newScore);
                    changedScores.Add(new ScoreKey { Player = player, Game = game });
                }
            }
            // Additions
            if (RandomBool())
                for (int i = 0, count = random.Next(10); i < count; i++)
                    addedPlayers.Add(AddNewPlayer());
            if (RandomBool())
                for (int i = 0, count = random.Next(5); i < count; i++)
                    addedGames.Add(AddNewGame());
            foreach (var game in addedGames)
                foreach (var player in addedPlayers)
                    SetScore(player, game, random.Next(1000));
            // Notify
            var handler = Changed;
            if (handler != null && (long)addedGames.Count + removedGames.Count + addedPlayers.Count + removedPlayers.Count + changedScores.Count > 0)
            {
                var e = new ScoreBoardChangeEventArgs { AddedPlayers = addedPlayers, RemovedPlayers = removedPlayers, AddedGames = addedGames, RemovedGames = removedGames, ChangedScores = changedScores };
                syncContext.Send(_ => handler(this, e), null);
            }
        }
        Random random = new Random();
        int playerId, gameId;
        bool RandomBool() { return (random.Next() % 5) == 0; }
        Player AddNewPlayer()
        {
            int id = ++playerId;
            var item = new Player(id) { Name = "P" + id };
            players.Add(item);
            return item;
        }
        Game AddNewGame()
        {
            int id = ++gameId;
            var item = new Game(id) { Name = "G" + id };
            games.Add(item);
            return item;
        }
        void SetScore(Player player, Game game, int score)
        {
            Dictionary<int, int> playerScores;
            if (!gameScores.TryGetValue(game.Id, out playerScores))
                gameScores.Add(game.Id, playerScores = new Dictionary<int, int>());
            playerScores[player.Id] = score;
        }
        #endregion
    }
    struct ScoreKey
    {
        public Player Player;
        public Game Game;
    }
    class ScoreBoardChangeEventArgs
    {
        public IReadOnlyList<Player> AddedPlayers, RemovedPlayers;
        public IReadOnlyList<Game> AddedGames, RemovedGames;
        public IReadOnlyList<ScoreKey> ChangedScores;
        public long Count { get { return (long)AddedPlayers.Count + RemovedPlayers.Count + AddedGames.Count + RemovedGames.Count + ChangedScores.Count; } }
    }
}  

The class in interest is StoreBoard. Basically it has Players and Games lists, GetScore function by (player, game), and multipurpose batch change notification. I want it to be presented in a tabular format with rows being players, columns - games, and their intersection - scores. Also all the updating should be done in a structured way (using some sort of data binding).

WF specific solution:

the view model: IList will handle the row part, ITypedList with custom PropertyDescriptors - column part, and IBindingList.ListChanged event - all modifications.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
namespace WfViewModels
{
    using Models;

    class ScoreBoardItemViewModel : CustomTypeDescriptor
    {
        ScoreBoardViewModel container;
        protected ScoreBoard source { get { return container.source; } }
        Player player;
        Dictionary<int, int> playerScores;
        public ScoreBoardItemViewModel(ScoreBoardViewModel container, Player player)
        {
            this.container = container;
            this.player = player;
            playerScores = new Dictionary<int, int>(source.Games.Count);
            foreach (var game in source.Games) AddScore(game);
        }
        public Player Player { get { return player; } }
        public int GetScore(Game game) { int value; return playerScores.TryGetValue(game.Id, out value) ? value : 0; }
        internal void AddScore(Game game) { playerScores.Add(game.Id, source.GetScore(player, game)); }
        internal bool RemoveScore(Game game) { return playerScores.Remove(game.Id); }
        internal bool UpdateScore(Game game)
        {
            int oldScore = GetScore(game), newScore = source.GetScore(player, game);
            if (oldScore == newScore) return false;
            playerScores[game.Id] = newScore;
            return true;
        }
        public override PropertyDescriptorCollection GetProperties()
        {
            return container.properties;
        }
    }
    class ScoreBoardViewModel : BindingList<ScoreBoardItemViewModel>, ITypedList
    {
        internal ScoreBoard source;
        internal PropertyDescriptorCollection properties;
        public ScoreBoardViewModel(ScoreBoard source)
        {
            this.source = source;
            properties = new PropertyDescriptorCollection(
                new[] { CreateProperty("PlayerName", item => item.Player.Name, "Player") }
                .Concat(source.Games.Select(CreateScoreProperty))
                .ToArray()
            );
            source.Changed += OnSourceChanged;
        }
        public void Load()
        {
            Items.Clear();
            foreach (var player in source.Players)
                Items.Add(new ScoreBoardItemViewModel(this, player));
            ResetBindings();
        }
        void OnSourceChanged(object sender, ScoreBoardChangeEventArgs e)
        {
            var count = e.Count;
            if (count == 0) return;
            RaiseListChangedEvents = count < 2;
            foreach (var player in e.RemovedPlayers) OnRemoved(player);
            foreach (var game in e.RemovedGames) OnRemoved(game);
            foreach (var game in e.AddedGames) OnAdded(game);
            foreach (var player in e.AddedPlayers) OnAdded(player);
            foreach (var group in e.ChangedScores.GroupBy(item => item.Player))
            {
                int index = IndexOf(group.Key);
                if (index < 0) continue;
                bool changed = false;
                foreach (var item in group) changed |= Items[index].UpdateScore(item.Game);
                if (changed) ResetItem(index);
            }
            if (RaiseListChangedEvents) return;
            RaiseListChangedEvents = true;
            if (e.AddedGames.Count + e.RemovedGames.Count > 0)
                OnListChanged(new ListChangedEventArgs(ListChangedType.PropertyDescriptorChanged, null));
            if ((long)e.AddedPlayers.Count + e.RemovedPlayers.Count + e.ChangedScores.Count > 0)
                ResetBindings();
        }
        void OnAdded(Player player)
        {
            if (IndexOf(player) >= 0) return;
            Add(new ScoreBoardItemViewModel(this, player));
        }
        void OnRemoved(Player player)
        {
            int index = IndexOf(player);
            if (index < 0) return;
            RemoveAt(index);
        }
        void OnAdded(Game game)
        {
            if (IndexOf(game) >= 0) return;
            var property = CreateScoreProperty(game);
            properties.Add(property);
            foreach (var item in Items)
                item.AddScore(game);
            if (RaiseListChangedEvents)
                OnListChanged(new ListChangedEventArgs(ListChangedType.PropertyDescriptorAdded, property));
        }
        void OnRemoved(Game game)
        {
            int index = IndexOf(game);
            if (index < 0) return;
            var property = properties[index];
            properties.RemoveAt(index);
            foreach (var item in Items)
                item.RemoveScore(game);
            if (RaiseListChangedEvents)
                OnListChanged(new ListChangedEventArgs(ListChangedType.PropertyDescriptorDeleted, property));
        }
        int IndexOf(Player player)
        {
            for (int i = 0; i < Count; i++)
                if (this[i].Player == player) return i;
            return -1;
        }
        int IndexOf(Game game)
        {
            var propertyName = ScorePropertyName(game);
            for (int i = properties.Count - 1; i >= 0; i--)
                if (properties[i].Name == propertyName) return i;
            return -1;
        }
        string ITypedList.GetListName(PropertyDescriptor[] listAccessors) { return null; }
        PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors) { return properties; }
        static string ScorePropertyName(Game game) { return "Game_" + game.Id; }
        static PropertyDescriptor CreateScoreProperty(Game game) { return CreateProperty(ScorePropertyName(game), item => item.GetScore(game), game.Name); }
        static PropertyDescriptor CreateProperty<T>(string name, Func<ScoreBoardItemViewModel, T> getValue, string displayName = null)
        {
            return new ScorePropertyDescriptor<T>(name, getValue, displayName);
        }
        class ScorePropertyDescriptor<T> : PropertyDescriptor
        {
            string displayName;
            Func<ScoreBoardItemViewModel, T> getValue;
            public ScorePropertyDescriptor(string name, Func<ScoreBoardItemViewModel, T> getValue, string displayName = null) : base(name, null)
            {
                this.getValue = getValue;
                this.displayName = displayName ?? name;
            }
            public override string DisplayName { get { return displayName; } }
            public override Type ComponentType { get { return typeof(ScoreBoardItemViewModel); } }
            public override bool IsReadOnly { get { return true; } }
            public override Type PropertyType { get { return typeof(T); } }
            public override bool CanResetValue(object component) { return false; }
            public override object GetValue(object component) { return getValue((ScoreBoardItemViewModel)component); }
            public override void ResetValue(object component) { throw new NotSupportedException(); }
            public override void SetValue(object component, object value) { throw new NotSupportedException(); }
            public override bool ShouldSerializeValue(object component) { return false; }
        }
    }
}

Side note: in the code above can be seen one of the WF databinding flaws - we are stuck with a singe item list change notifications, which is ineffective if there are a lot of changes to apply, or brute force Reset notification which cannot be handled effectively by any list data presenter.

the view:

using System;
using System.Drawing;
using System.Windows.Forms;
namespace Views
{
    using Models;
    using ViewModels;
    class ScoreBoardView : Form
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new ScoreBoardView { WindowState = FormWindowState.Maximized });
        }
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            var source = new ScoreBoard();
            viewModel = new ScoreBoardViewModel(source);
            InitView();
            viewModel.Load();
            source.StartUpdate();
        }
        ScoreBoardViewModel viewModel;
        DataGridView view;
        void InitView()
        {
            view = new DataGridView { Dock = DockStyle.Fill, Parent = this };
            view.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
            view.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
            view.MultiSelect = false;
            view.CellBorderStyle = DataGridViewCellBorderStyle.None;
            view.ForeColor = Color.Black;
            view.AllowUserToAddRows = view.AllowUserToDeleteRows = view.AllowUserToOrderColumns = view.AllowUserToResizeRows = false;
            view.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
            view.RowHeadersVisible = false;
            view.EnableHeadersVisualStyles = false;
            var style = view.DefaultCellStyle;
            style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
            style = view.ColumnHeadersDefaultCellStyle;
            style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
            style.BackColor = Color.Navy;
            style.ForeColor = Color.White;
            style = view.RowHeadersDefaultCellStyle;
            style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
            style = view.RowsDefaultCellStyle;
            style.SelectionForeColor = style.ForeColor = Color.Black;
            style.SelectionBackColor = style.BackColor = Color.AliceBlue;
            style = view.AlternatingRowsDefaultCellStyle;
            style.SelectionForeColor = style.ForeColor = Color.Black;
            style.SelectionBackColor = style.BackColor = Color.LightSteelBlue;
            view.ColumnAdded += OnViewColumnAdded;
            view.DataSource = viewModel;
            view.AutoResizeColumnHeadersHeight();
            view.RowTemplate.MinimumHeight = view.ColumnHeadersHeight;
        }
        private void OnViewColumnAdded(object sender, DataGridViewColumnEventArgs e)
        {
            var column = e.Column;
            if (column.ValueType == typeof(int))
            {
                var style = column.DefaultCellStyle;
                style.Alignment = DataGridViewContentAlignment.MiddleRight;
                style.Format = "n0";
            }
        }
    }
}  

And that's it.

Looking forward for the WPF way. And please note that this question is not for "which is better" comparison between WF and WPF - I'm really interested in WPF solution(s) of the problem.

EDIT: In fact, I was wrong. My "view model" is not WF specific. I've updated it with a cosmetic change (using ICustomTypeDescriptor) and now it's usable in both WF and WPF.

Community
  • 1
  • 1
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343

1 Answers1

4

So, your solution is extremely convoluted and resorts to hacks such as using reflection, which doesn't really surprise me since winforms is a very outdated technology and requires such hacks for everything.

WPF is a modern UI framework and does not need any of that.

This is a very naive solution that I put together in 15 minutes. Notice that it has absolutely zero performance considerations (since I'm basically throwing away and recreating all the rows and columns constantly) and yet the UI remains totally responsive while running.

First of all some base support for DataBinding:

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

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

Notice that this class requires nothing more than literally Ctrl+Enter since ReSharper puts that boilerplate in place automatically.

Then, using the same Model classes you provided, I put together this ViewModel:

public class ViewModel : PropertyChangedBase
{
    private readonly ScoreBoard board;

    public ObservableCollection<string> Columns { get; private set; }

    public ObservableCollection<Game> Games { get; private set; } 

    public ObservableCollection<RowViewModel> Rows { get; private set; } 

    public ViewModel(ScoreBoard board)
    {
        this.board = board;
        this.board.Changed += OnBoardChanged;

        UpdateColumns(this.board.Games.Select(x => x.Name));
        UpdateRows(this.board.Players, this.board.Games);

        this.board.StartUpdate();
    }

    private void OnBoardChanged(object sender, ScoreBoardChangeEventArgs e)
    {
        var games = 
            this.board.Games
                      .Except(e.RemovedGames)
                      .Concat(e.AddedGames)
                      .ToList();

        this.UpdateColumns(games.Select(x => x.Name));

        var players =
            this.board.Players
                      .Except(e.RemovedPlayers)
                      .Concat(e.AddedPlayers)
                      .ToList();

        this.UpdateRows(players, games);
    }

    private void UpdateColumns(IEnumerable<string> columns)
    {
        this.Columns = new ObservableCollection<string>(columns);
        this.Columns.Insert(0, "Player");

        this.OnPropertyChanged("Columns");
    }

    private void UpdateRows(IEnumerable<Player> players, IEnumerable<Game> games)
    {
        var rows =
            from p in players
            let scores =
                from g in games
                select this.board.GetScore(p, g)
            let row = 
                new RowViewModel
                {
                    Player = p.Name,
                    Scores = new ObservableCollection<int>(scores)
                }
            select row;

        this.Rows = new ObservableCollection<RowViewModel>(rows);
        this.OnPropertyChanged("Rows");
    }
}

public class RowViewModel
{
    public string Player { get; set; }

    public ObservableCollection<int> Scores { get; set; }
}

Then some XAML:

<Window x:Class="WpfApplication31.Window3"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window3" Height="300" Width="300">
    <Window.Resources>
        <Style TargetType="ItemsControl" x:Key="Horizontal">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="ListBoxItem">
            <Setter Property="Padding" Value="0"/>
        </Style>

        <DataTemplate x:Key="CellTemplate">
            <Border BorderBrush="Black" BorderThickness="1" Padding="5" Width="60">
                <TextBlock Text="{Binding}"
                           VerticalAlignment="Center"
                           HorizontalAlignment="Center"/>
            </Border>
        </DataTemplate>
    </Window.Resources>

    <DockPanel>
        <ItemsControl ItemsSource="{Binding Columns}"
                      Style="{StaticResource Horizontal}"
                      Margin="3,0,0,0"
                      ItemTemplate="{StaticResource CellTemplate}"
                      DockPanel.Dock="Top"/>

        <ListBox ItemsSource="{Binding Rows}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <ContentPresenter Content="{Binding Player}"
                                          ContentTemplate="{StaticResource CellTemplate}"/>

                        <ItemsControl ItemsSource="{Binding Scores}"
                                  Style="{StaticResource Horizontal}"
                                  ItemTemplate="{StaticResource CellTemplate}"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </DockPanel>
</Window>

Notice that, while this looks like a lot of XAML, I'm not using the built-in DataGrid or any other built-in control, but rather putting it together myself using nested ItemsControls.

Finally, the Window's code behind, which simply instantiates the VM and sets the DataContext:

public partial class Window3 : Window
{
    public Window3()
    {
        InitializeComponent();

        var board = new ScoreBoard();
        this.DataContext = new ViewModel(board);
    }
}

Result:

enter image description here

  • The first ItemsControl shows the Columns collection (the column names) on top.
  • The ListBox shows the Rows, each row containing a single cell for the player name, and then an horizontal ItemsControl for the numeric cells. Notice that in contrast to the winforms' counterpart, the WPF ListBox is actually useful.
  • Notice that my solution supports row selection like a standard DataGrid would, except that because I'm throwing away and recreating the entire dataset constantly, the selection is not maintained throughout. I could add a SelectedRow property in the VM to fix this.
  • Notice that my totally naive example with no optimizations whatsoever is more than capable of dealing with your 100 ms update cycle. If data was larger performance would surely start to degrade, and a better solution would be required, such as actually deleting what needs to be deleted and adding what needs to be added. Notice that even with a more complex solution I still wouldn't need to use reflection or any other hacks.
  • Also notice that my ViewModel code is much shorter (95 LOC versus 154 of yours) and I did not resort to deleting all blank lines to make it look shorter.
Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154
  • Thanks, that's exactly what I was looking for. Looks pretty good! Your code is very clean and much convincing than your style of speaking. Frankly, I don't understand why you repeat one and the same all the time. I explicitly stated that this question is not for defensive comparison - neither performance, nor lines of code etc. Also I would be glad if you show me where exactly you saw reflection or horrible hacks in my code. At least you do not complain about my model :-) – Ivan Stoev Aug 23 '15 at 23:46
  • But let put aside the offensive style and do some constructive discussion, ok? And let use WPF and WF instead of "my" and "yours". Actually let forget WF at all. Now, I have the following question. I don't see how the layout of the different `ItemsControl`s cells is correlated. Are they correlated at all? Also what about autosizing columns based on the content (side note - that was so far the main performance affecting factor in WF. in fact unusable with more than hundred rows). – Ivan Stoev Aug 24 '15 at 11:08
  • @IvanStoev no, they happen to fit together because I'm using a fixed size. If you want to do autosizing of columns you'll have to use `ShareSizeScope`. – Federico Berasategui Aug 24 '15 at 14:31
  • Another thing. I tried to bind your `ViewModel` to a WPF `DataGrid` by simply dropping a grid control (called "view") on the `Window` design canvas and using `view.ItemSource = viewModel.Rows` in the constructor. It doesn't work - the grid shows 2 columns - "Player" and "Scores". Rows show correct values in the first one and always "(Collection)" in the second. Btw, I should have mentioned that I'm not a UI developer, so I'm not very interested in the "view" part of the MVVM. The "view model" is my main target, I need to know how to control the UI from there. – Ivan Stoev Aug 24 '15 at 18:05
  • @IvanStoev the `DataGrid` shows `(Collection)` in the cell because it doesn't "know" how to render the Scores property. You need a `DataTemplate` for that. If you're not a "UI guy", maybe you need a "UI guy" in your team to write this XAML? ;) – Federico Berasategui Aug 24 '15 at 18:14
  • I'm in vacation and just researching. I think you can learn something from me too. I've looked at the documentation and played a bit, and from what I saw, there is no fundamental difference between WPF and WF data binding models. In fact both are built around `System.ComponentModel` when you bind to a POCOs (like your `RowViewModel`) and use `PropertyDescriptor`(which by default are implemented with your favorite **reflection** :-) In fact, I was able to bind the WPF grid to my "view model" w/o a need to write XAML. The VM needed a cosmetic change because WPF needs ICustomTypeDescriptor. – Ivan Stoev Aug 24 '15 at 18:36
  • and it's working - except dynamic add/remove columns. cool. I'll update the code in the question. if you don't trust me - implement explicitly Player property, put a breakpoint on the getter and see what is in the callstack. or just read this https://msdn.microsoft.com/en-us/library/bb613546(v=vs.100).aspx – Ivan Stoev Aug 24 '15 at 18:42
  • @IvanStoev winforms' databinding is useless. You can't bind to `Property1.Property2.Property3`, in WPF you can. Also, WPF has the `ItemsControl` and the entire concept of Data Templates which has no equivalent in winforms. Also WPF has built in UI virtualization that makes winforms look like a pathetic dinousaur. – Federico Berasategui Aug 24 '15 at 18:43
  • @IvanStoev also, winforms' databinding is limited to certain types of controls, whereas in WPF I can even bind animations' or transforms' properties, and have, for example, a UI element visually rotating in the screen as per some `Angle` property in the ViewModel. – Federico Berasategui Aug 24 '15 at 18:46
  • As I said, I don't care about the visual aspects, so I trust you. But I need something that can handle millions of rows with thousands of columns **data** and to be controlled by the model. In that regard, the data binding models are one and the same. Which is good I think, because it provides a good transition plan (do you ever thought that the people can be bound to a specific technology by an external factors like a long term projects etc.) – Ivan Stoev Aug 24 '15 at 19:02