One of the recent questions I saw in Stackoverflow involved a scenario wherein, the developer had to reuse the same View for different ViewModels. In his case, the ViewModels where subtypes of same BaseViewModel and hence it made sense to reuse the View.
For example, Consider the following
public class TempatureViewModel:VariableViewModel{} public class PressureViewModel:VariableViewModel{} public class HumidityViewModel:VariableViewModel{}
For all 3 viewmodels, he would like to use the View VariableView
, which was the View associated with the VariableViewModel
as per the default naming convention used by Caliburn Micro.
Caliburn Micro, however allows one to override these default naming convention rules with ones own. At the same time, I wanted to develop a solution which more verbose and not hidden away completely in the Bootstrapper.
So my suggestion was to begin by adding a new Attribute to the solution UseViewOfAttribute
, which is defined as
[AttributeUsage(AttributeTargets.Class,AllowMultiple =false)] public class UseViewOfAttribute:Attribute { public Type SelectedType { get; set; } public UseViewOfAttribute(Type type) => SelectedType = type; }
The purpose of the attribute would be to allow the subtype ViewModels to specify which ViewModel should be used as candidate. Of course you could also look up the inheritance chain (which I have explained in the last part), but I felt the attribute driven approach makes it more verbose and hence increases the readability of code.
We will now continue to decorate our subtype viewmodels with the attribute we have introduced.
[UseViewOf(typeof(VariableViewModel))] public class TemperatureVariableViewModel: VariableViewModel { }
Now you could move over to your Bootstrapper and override the definition of existing ViewLocator method as the following.
var existingViewLocator = ViewLocator.LocateTypeForModelType; ViewLocator.LocateTypeForModelType = (modelType, displayLocation, context) => { var targetType = existingViewLocator(modelType, displayLocation, context); if(targetType == null && modelType.GetCustomAttributes<UseViewOfAttribute>().Any()) { var attribute = modelType.GetCustomAttribute<UseViewOfAttribute>(); targetType = existingViewLocator(attribute.SelectedType,displayLocation,context); } return targetType; };
This would ensure that now you could use the same Parent View for all the sub types which are decorated with the attribute.
Alternate Approach
As mentioned earlier, there is the possibility to skip the creation of the attribute and walk the inheritance chain to detect the View for the ViewModel. For example, you could change the ViewLocator as the following.
var existingViewLocator = ViewLocator.LocateTypeForModelType; ViewLocator.LocateTypeForModelType = (modelType, displayLocation, context) => { var targetType = existingViewLocator(modelType, displayLocation, context); if(targetType == null) { while(targetType == null) { modelType = modelType.BaseType; targetType = existingViewLocator(modelType, displayLocation, context); } } return targetType; };
The problem with this approach are
- You are not entire aware (or rather make it explicit to anyone reading your code) of how many levels of inheritance you need to go back. There could multiple base ViewModels each having its own Views. The attribute based approach on other hand, explicitly states the ViewModel to inspect.
- There could subtypes which needs to use its own Views or entirely View for that matter. This again its problem with parsing the inheritance chain, but is automatically solved if using the attribute based approach.
In opinion, the attribute based is a much more convenient and better approach because of its verbose nature and flexibility. On other hand, this is a compile time approach, and an even better solution would be a run time solution, which we will address in one of the future posts.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.