0

I am currently refactoring my codes to be a SOLID compliant. SOLID principle has bee a practice by most of the developers and I neglect to learn this principle. But now I need it!

My ViewModel contains Events, Fields, Properties (ObseravbleCollection and other data types), ICommand, Methods which are made for a specific View and some of ViewModel contains Properties and Methods only if the collection will be used in other views. So it's more like a reusable VM for some lists. i.e. A view where managing employees and another view for listing employees for other purposes.

I am trying to understand and made a simple Login UI using WPF and my ViewModel looks like this.

using GalaSoft.MvvmLight.Command;

using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;

using UnderstandingSOLID.Models;
using UnderstandingSOLID.Services.API;

namespace UnderstandingSOLID.ViewModels
{
    public class ViewModel_Login : VMBase
    {
        #region events
        public EventHandler OnLoggedIn;
        #endregion

        #region vars

        #endregion

        #region properties
        public ObservableCollection<Model_ServerType> ServerTypes { get; set; } = new ObservableCollection<Model_ServerType>();

        private Model_ServerType _SelectedServerType = new Model_ServerType();
        public Model_ServerType SelectedServerType
        {
            get { return _SelectedServerType; }
            set
            {
                Set(nameof(SelectedServerType), ref _SelectedServerType, value);
            }
        }

        private string _ServerName = null;
        public string ServerName
        {
            get { return _ServerName; }
            set { Set(nameof(ServerName), ref _ServerName, value); }
        }

        private string _Username = null;
        public string Username
        {
            get { return _Username; }
            set { Set(nameof(Username), ref _Username, value); }
        }

        private string _Password = null;
        public string Password
        {
            get { return _Password; }
            set { Set(nameof(Password), ref _Password, value); }
        }

        private bool _ShowLoginUI = false;
        public bool ShowLoginUI
        {
            get { return _ShowLoginUI; }
            set { Set(nameof(ShowLoginUI), ref _ShowLoginUI, value); }
        }
        #endregion

        #region commands
        public ICommand Command_Connect { get; set; }
        public ICommand Command_Help { get; set; }
        #endregion

        #region ctors
        public ViewModel_Login()
        {
            InitCommands();

            // used only in UWP & WPF
            // or anything that supports design time updates
            if (base.IsInDesignMode)
            {
                DesignData();
            }
        }
        #endregion

        #region command methods
        void Command_Connect_Click()
        {
            Connect();
        }

        void Command_Help_Click()
        {
            // does nothing at the moment
        }
        #endregion

        #region methods
        void InitCommands()
        {
            if (Command_Connect == null) Command_Connect = new RelayCommand(Command_Connect_Click);
            if (Command_Help == null) Command_Help = new RelayCommand(Command_Help_Click);
        }

        /// <summary>
        /// codes here are messy since they are only used for desining the UI
        /// </summary>
        void DesignData()
        {
            this.ShowLoginUI = true;
            this.ServerTypes.Clear();

            string[] serverTypes = new string[]
            {
                "Database Engine",
                "Analysis Services"
            };

            for (int i = 0; i < serverTypes.Length; i++)
            {
                this.ServerTypes.Add(new Model_ServerType()
                {
                    Id = i,
                    ServerType = serverTypes[i]
                });
            }

            this.ShowDlgMsg("Title", "Failed to login");
        }

        // this is called in Loaded event in MainPage.xaml.cs
        public async Task RefreshData()
        {
            this.ServerTypes.Clear();

            string[] serverTypes = new string[]
            {
                "Database Engine",
                "Analysis Services",
                "Reporting Services",
                "Integration Services"
            };

            for(int i = 0; i < serverTypes.Length; i++)
            {
                this.ServerTypes.Add(new Model_ServerType()
                {
                    Id = i,
                    ServerType = serverTypes[i]
                });
            }
        }

        async Task Connect()
        {
            if(this.SelectedServerType.ServerType != null && await ApiClient.I.Login(this.Username, this.Password))
            {
                this.HideDlgMsg();

                // navigate to main page
                // but we'll just Invoke an event for simplicity

                this.OnLoggedIn?.Invoke(this, null);
            }
            else
            {
                this.ShowDlgMsg("Error", "Failed to login");
            }
        }
        #endregion
    }
}

VMBase looks like this

using GalaSoft.MvvmLight;

namespace UnderstandingSOLID.ViewModels
{
    public abstract class VMBase : ViewModelBase
    {
        private bool _ShowMessage = false;
        public bool ShowMessage
        {
            get { return _ShowMessage; }
            set { Set(nameof(ShowMessage), ref _ShowMessage, value); }
        }

        private string _MessageTitle = null;
        public string MessageTitle
        {
            get { return _MessageTitle; }
            set { Set(nameof(MessageTitle), ref _MessageTitle, value); }
        }

        private string _MessageBody = null;
        public string MessageBody
        {
            get { return _MessageBody; }
            set { Set(nameof(MessageBody), ref _MessageBody, value); }
        }

        public virtual void ShowDlgMsg(string title, string message)
        {
            this.ShowMessage = true;
            this.MessageTitle = title;
            this.MessageBody = message;
        }

        public void HideDlgMsg()
        {
            this.ShowMessage = false;
        }
    }
}

So that's how I normally write all the members in my ViewModel. Don't mind how I called the Login in Connect method in ViewModel_Login. It was only simplified.

So how do you implement SOLID principle in this case?

The sample login app looks like this in idle enter image description here

login failed enter image description here

logged in enter image description here

can download the repo here https://github.com/Nullstr1ng/UnderstandingSOLID

jaysonragasa
  • 1,076
  • 1
  • 20
  • 40
  • 1
    Have a look at https://github.com/SirRufo/CleanArchitecture/tree/master/Desktop that should give you an idea how to implement it – Sir Rufo Apr 05 '20 at 12:14

1 Answers1

1

Programming principles are like pirate code: they are more like guidelines than actual rules - they don't always improve things.

Your VM looks ok - that's how you do ViewModels. I use different framework, so I would do some things differently, but:

  • you don't use view class reference in ViewModel = GREAT! (you should still show us the View, at least CodeBehind since this is the most tricky part in MVVM)
  • APIClient should be a dependency, an instance should have been passed in constructor as and interface od IAPIClient as opposed to static property
  • if you really want to follow SOLID, each ViewModel should have its own interface: IViewModel_Login, containing all public methods, properties and events that the view uses (this actually can be used to make ViewModel_Login_Design class that you can specify in XAML that just returns some static values [there is d:DataContext])
  • you should extract a class to handle the message from VMBase - and maybe not use it in the base, but inject it to ViewModels that use it. Base should only have things that are common to almost ALL ViewModels.

I will also put a link to ReactiveUI here, since I strongly believe that its design and it being focused around IObservable and using DynamicData for dynamic collections is far superior experience to any other MVVM framework.

Krzysztof Skowronek
  • 2,796
  • 1
  • 13
  • 29
  • I'm just a little hesitant with using these kinds of libraries. Aren't these going to slow things down? It may not be really that noticeable with our processors these days. There's so many things going on in the background just by checking the codes behind these extension methods. this.WhenAnyValue(x => x.SearchQuery) .Throttle(TimeSpan.FromSeconds(0.8), RxApp.TaskpoolScheduler) .Select(query => query?.Trim()) .DistinctUntilChanged() .Where(query => !string.IsNullOrWhiteSpace(query)) .ObserveOn(RxApp.MainThreadScheduler) .InvokeCommand(ExecuteSearch); – jaysonragasa Apr 05 '20 at 13:56
  • If you want the same effect, you have to the same job in some way. The code above is very readable, but of course it can be done in a "normal way". But then you need to manage a timer, if you don't put the code explicit on the bakground thread, in WPF there is good change that it will run on UI thread. No, if you do it right, it will go very fast. If you do it bad, it will go slow no matter the tech. In WPF you almost aways hit UI rendering as a bottleneck, not the background timers and filtering. – Krzysztof Skowronek Apr 05 '20 at 14:11