Reputation: 35821
I'm using MVVM Light in Visual Studio 2015 to build an WPF app. There's a method in the code that's repeated with slight variation 4 times in the code; the only difference is the type of the ObservableCollection
being modified and the method called on the data service layer.
Here's the method, which returns an ObservableCollection
of StatusViewModel
objects, which are used to populate a ComboBox
; the StatusVm
is used for binding to the SelectedItem
of the ComboBox
, is set as the first item in the collection and is "blank":
private async Task<ObservableCollection<StatusViewModel>> GetStatuses()
{
var result = new ObservableCollection<StatusViewModel>();
var blank = new StatusViewModel
{
StatusId = -1,
Status = null,
Description = null,
IsActive = false,
CreatedDate = DateTime.Now
};
result.Add(blank);
var dataService = new MyDataService();
foreach (var c in await dataService.GetStatuses())
result.Add(c);
StatusVm =
result.SingleOrDefault(c => c.StatusId.Equals(-1));
return result;
}
Here's the private field and public property for StatusVm
:
private StatusViewModel _statusVm;
public StatusViewModel StatusVm
{
get { return _statusVm; }
set
{
if (Equals(value, _statusVm)) return;
_statusVm = value;
RaisePropertyChanged();
}
}
Now imagine the above being repeated 3 more times, with 3 more VM types! How do I make GetStatuses()
into a method that can take different view model types and call the appropriate method on the data service? Thank you.
Update: Here are the property and method for another of the types:
private MroViewModel_mroVm;
public MroViewModel MroVm
{
get { return _mroVm; }
set
{
if (Equals(value, _mroVm)) return;
_mroVm = value;
RaisePropertyChanged();
}
}
private async Task<ObservableCollection<MroViewModel>> GetMro()
{
var result = new ObservableCollection<MroViewModel>();
var blank = new MroViewModel
{
StatusId = -1,
Status = null,
Description = null,
IsActive = false,
CreatedDate = DateTime.Now
};
result.Add(blank);
var dataService = new MyDataService();
foreach (var c in await dataService.GetMro())
result.Add(c);
MroVm =
result.SingleOrDefault(c => c.StatusId.Equals(-1));
return result;
}
Upvotes: 1
Views: 415
Reputation: 781
You could define interfaces and try a combo of Strategy
and Factory
, like the following (I skipped async/await for simplicity):
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Starting");
var mainVm = new MainViewModel();
mainVm.GetStatuses();
mainVm.GetMro();
Console.WriteLine("Status: {0} {1}", mainVm.StatusVm.Name, mainVm.StatusVm.CreateDate);
Console.WriteLine("MroVm: {0} {1}", mainVm.MroVm.Name, mainVm.MroVm.CreateDate);
}
}
public class MainViewModel
{
public StatusViewModel StatusVm { get; set; }
public MroViewModel MroVm { get; set; }
public void GetStatuses()
{
var result = Get(VmKind.Status);
StatusVm = result.SingleOrDefault(c => c.StatusId.Equals(-1)) as StatusViewModel;
}
public void GetMro()
{
var result = Get(VmKind.Mro);
MroVm = result.SingleOrDefault(c => c.StatusId.Equals(-1)) as MroViewModel;
}
public IEnumerable<IVm> Get(VmKind vmKind)
{
var dataService = new MyDataService();
return dataService.Get(vmKind);
}
}
public interface IVm
{
int StatusId { get; set; }
DateTime CreateDate { get; set; }
string Name { get; }
}
public class StatusViewModel : IVm
{
public DateTime CreateDate { get; set; }
public int StatusId { get; set; }
public string Name { get { return "StatusViewModel"; } }
}
public class MroViewModel : IVm
{
public DateTime CreateDate { get; set; }
public int StatusId { get; set; }
public string Name { get { return "MroViewModel"; } }
}
public enum VmKind {Status, Mro }
#region Strategy
public interface IDataGetter
{
IEnumerable<IVm> Get(VmKind vmKind);
}
public class MyDataService : IDataGetter {
public IEnumerable<IVm> Get(VmKind vmKind)
{
switch (vmKind)
{
case VmKind.Status:
return GetStatuses();
//break;
case VmKind.Mro:
return GetMro();
//break;
default:
throw new ArgumentException("Unknown VM type");
}
}
private IEnumerable<IVm> GetMro()
{
return new List<MroViewModel> {
new MroViewModel { StatusId = -1, CreateDate = DateTime.Now },
new MroViewModel { StatusId = 2, CreateDate = DateTime.Now }
};
}
private IEnumerable<StatusViewModel> GetStatuses()
{
return new List<StatusViewModel> {
new StatusViewModel { StatusId = -1, CreateDate = DateTime.Now },
new StatusViewModel { StatusId = 2, CreateDate = DateTime.Now }
};
}
}
#endregion
#region Factory
public class VmFactory {
static IVm Create(VmKind vmKind)
{
IVm result = null;
switch (vmKind)
{
case VmKind.Status:
result = new StatusViewModel { StatusId = -1, CreateDate = DateTime.Now };
break;
case VmKind.Mro:
result = new MroViewModel { StatusId = -1, CreateDate = DateTime.Now };
break;
default:
throw new ArgumentException("Unknown VM type");
//break;
}
return result;
}
}
#endregion
I didn't actually use the Factory here, but you could do it for easy VM creation.
Upvotes: 1
Reputation: 2821
You would create interface for common properties.
internal interface IStatusViewModel {
int StatusId { get; set; }
string Status { get; set; }
string Description { get; set; }
bool IsActive { get; set; }
DateTime CreatedDate { get; set; }
}
Implement the interface in classes that you need to retrieve status for
internal class MroViewModel : IStatusViewModel {
public int StatusId { get; set; }
public string Status { get; set; }
public string Description { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedDate { get; set; }
}
Make method static and pass the service function which will call appropriate method to retrieve old statuses.
public static async Task<ObservableCollection<T>> GetStatuses<T>(
Func<MyDataService, Task<IEnumerable<T>>> retrieveStatusesAction)
where T : IStatusViewModel, new()
{
var result = new ObservableCollection<T>();
var blank = new T
{
StatusId = -1,
Status = null,
Description = null,
IsActive = false,
CreatedDate = DateTime.Now
};
result.Add(blank);
var dataService = new MyDataService();
foreach (var c in await retrieveStatusesAction(dataService))
result.Add(c);
// TODO Implement Expression<Func<TSource, TResult>> projection for assigning to VM
StatusVm = result.SingleOrDefault(c => c.StatusId.Equals(-1));
return result;
}
You would then call this method like so:
GetStatuses((service) => service.GetMro());
I didn't test this and the StatusVm needs to be assigned using expression compilation. I will take a look at how to do that now, but the idea is there.
For the Expression and property assigning: Property selector Expression<Func<T>>. How to get/set value to selected property
-- EDIT --
Something like this for VM assignment:
public static async Task<ObservableCollection<T>> GetStatuses<T, TContainer>(
TContainer instance,
Expression<Func<TContainer, T>> viewModelProjection,
Func<MyDataService, Task<IEnumerable<T>>> retrieveStatusesAction)
where T : IStatusViewModel, new()
{
var result = new ObservableCollection<T>();
var blank = new T
{
StatusId = -1,
Status = null,
Description = null,
IsActive = false,
CreatedDate = DateTime.Now
};
result.Add(blank);
var dataService = new MyDataService();
foreach (var c in await retrieveStatusesAction(dataService))
result.Add(c);
var vmStatus = result.SingleOrDefault(c => c.StatusId.Equals(-1));
// Warning: Check casted values, this is unsafe
var vm = (PropertyInfo)((MemberExpression)viewModelProjection.Body).Member;
vm.SetValue(instance, vmStatus, null);
return result;
}
You would then call the method like this:
await GetStatuses(this, inst => inst.MroVm, (service) => service.GetMro());
If you are not familiar with expressions, I will explain. First argument is object in which view model instance is located. Second argument selects the property from that object that corresponds to the view model that needs to be changed. Last argument is function that takes the service and returns appropriate method that retrieves the statuses - this is like pointer to the function in C++.
This will compile, but not sure if it will behave as expected. It should. If you have any problems, please write them down in comments.
Upvotes: 3