ViewLocator.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. #if XFORMS
  2. namespace Caliburn.Micro.Core.Xamarin.Forms
  3. #else
  4. namespace Caliburn.Micro
  5. #endif
  6. {
  7. using System;
  8. using System.Linq;
  9. using System.Reflection;
  10. using System.Text.RegularExpressions;
  11. using System.Collections.Generic;
  12. #if XFORMS
  13. using global::Xamarin.Forms;
  14. using UIElement = global::Xamarin.Forms.Element;
  15. using TextBlock = global::Xamarin.Forms.Label;
  16. using DependencyObject = global::Xamarin.Forms.BindableObject;
  17. #elif !WinRT
  18. using System.Windows;
  19. using System.Windows.Controls;
  20. #else
  21. using Windows.UI.Xaml;
  22. using Windows.UI.Xaml.Controls;
  23. #endif
  24. #if !SILVERLIGHT && !WinRT && !XFORMS
  25. using System.Windows.Interop;
  26. using Caliburn.Micro.Core;
  27. #endif
  28. /// <summary>
  29. /// A strategy for determining which view to use for a given model.
  30. /// </summary>
  31. public static class ViewLocator {
  32. static readonly ILog Log = LogManager.GetLog(typeof(ViewLocator));
  33. static Dictionary<Type, UIElement> ViewList = new Dictionary<Type, UIElement>();
  34. //These fields are used for configuring the default type mappings. They can be changed using ConfigureTypeMappings().
  35. static string defaultSubNsViews;
  36. static string defaultSubNsViewModels;
  37. static bool useNameSuffixesInMappings;
  38. static string nameFormat;
  39. static string viewModelSuffix;
  40. static readonly List<string> ViewSuffixList = new List<string>();
  41. static bool includeViewSuffixInVmNames;
  42. ///<summary>
  43. /// Used to transform names.
  44. ///</summary>
  45. public static NameTransformer NameTransformer = new NameTransformer();
  46. /// <summary>
  47. /// Separator used when resolving View names for context instances.
  48. /// </summary>
  49. public static string ContextSeparator = ".";
  50. static ViewLocator() {
  51. ConfigureTypeMappings(new TypeMappingConfiguration());
  52. }
  53. /// <summary>
  54. /// Specifies how type mappings are created, including default type mappings. Calling this method will
  55. /// clear all existing name transformation rules and create new default type mappings according to the
  56. /// configuration.
  57. /// </summary>
  58. /// <param name="config">An instance of TypeMappingConfiguration that provides the settings for configuration</param>
  59. public static void ConfigureTypeMappings(TypeMappingConfiguration config) {
  60. if (String.IsNullOrEmpty(config.DefaultSubNamespaceForViews)) {
  61. throw new ArgumentException("DefaultSubNamespaceForViews field cannot be blank.");
  62. }
  63. if (String.IsNullOrEmpty(config.DefaultSubNamespaceForViewModels)) {
  64. throw new ArgumentException("DefaultSubNamespaceForViewModels field cannot be blank.");
  65. }
  66. if (String.IsNullOrEmpty(config.NameFormat)) {
  67. throw new ArgumentException("NameFormat field cannot be blank.");
  68. }
  69. NameTransformer.Clear();
  70. ViewSuffixList.Clear();
  71. defaultSubNsViews = config.DefaultSubNamespaceForViews;
  72. defaultSubNsViewModels = config.DefaultSubNamespaceForViewModels;
  73. nameFormat = config.NameFormat;
  74. useNameSuffixesInMappings = config.UseNameSuffixesInMappings;
  75. viewModelSuffix = config.ViewModelSuffix;
  76. ViewSuffixList.AddRange(config.ViewSuffixList);
  77. includeViewSuffixInVmNames = config.IncludeViewSuffixInViewModelNames;
  78. SetAllDefaults();
  79. }
  80. private static void SetAllDefaults() {
  81. if (useNameSuffixesInMappings) {
  82. //Add support for all view suffixes
  83. ViewSuffixList.Apply(AddDefaultTypeMapping);
  84. }
  85. else {
  86. AddSubNamespaceMapping(defaultSubNsViewModels, defaultSubNsViews);
  87. }
  88. }
  89. /// <summary>
  90. /// Adds a default type mapping using the standard namespace mapping convention
  91. /// </summary>
  92. /// <param name="viewSuffix">Suffix for type name. Should be "View" or synonym of "View". (Optional)</param>
  93. public static void AddDefaultTypeMapping(string viewSuffix = "View") {
  94. if (!useNameSuffixesInMappings) {
  95. return;
  96. }
  97. //Check for <Namespace>.<BaseName><ViewSuffix> construct
  98. AddNamespaceMapping(String.Empty, String.Empty, viewSuffix);
  99. //Check for <Namespace>.ViewModels.<NameSpace>.<BaseName><ViewSuffix> construct
  100. AddSubNamespaceMapping(defaultSubNsViewModels, defaultSubNsViews, viewSuffix);
  101. }
  102. /// <summary>
  103. /// This method registers a View suffix or synonym so that View Context resolution works properly.
  104. /// It is automatically called internally when calling AddNamespaceMapping(), AddDefaultTypeMapping(),
  105. /// or AddTypeMapping(). It should not need to be called explicitly unless a rule that handles synonyms
  106. /// is added directly through the NameTransformer.
  107. /// </summary>
  108. /// <param name="viewSuffix">Suffix for type name. Should be "View" or synonym of "View".</param>
  109. public static void RegisterViewSuffix(string viewSuffix) {
  110. if (ViewSuffixList.Count(s => s == viewSuffix) == 0) {
  111. ViewSuffixList.Add(viewSuffix);
  112. }
  113. }
  114. /// <summary>
  115. /// Adds a standard type mapping based on namespace RegEx replace and filter patterns
  116. /// </summary>
  117. /// <param name="nsSourceReplaceRegEx">RegEx replace pattern for source namespace</param>
  118. /// <param name="nsSourceFilterRegEx">RegEx filter pattern for source namespace</param>
  119. /// <param name="nsTargetsRegEx">Array of RegEx replace values for target namespaces</param>
  120. /// <param name="viewSuffix">Suffix for type name. Should be "View" or synonym of "View". (Optional)</param>
  121. public static void AddTypeMapping(string nsSourceReplaceRegEx, string nsSourceFilterRegEx, string[] nsTargetsRegEx, string viewSuffix = "View") {
  122. RegisterViewSuffix(viewSuffix);
  123. var replist = new List<string>();
  124. var repsuffix = useNameSuffixesInMappings ? viewSuffix : String.Empty;
  125. const string basegrp = "${basename}";
  126. foreach (var t in nsTargetsRegEx) {
  127. replist.Add(t + String.Format(nameFormat, basegrp, repsuffix));
  128. }
  129. var rxbase = RegExHelper.GetNameCaptureGroup("basename");
  130. var suffix = String.Empty;
  131. if (useNameSuffixesInMappings) {
  132. suffix = viewModelSuffix;
  133. if (!viewModelSuffix.Contains(viewSuffix) && includeViewSuffixInVmNames) {
  134. suffix = viewSuffix + suffix;
  135. }
  136. }
  137. var rxsrcfilter = String.IsNullOrEmpty(nsSourceFilterRegEx)
  138. ? null
  139. : String.Concat(nsSourceFilterRegEx, String.Format(nameFormat, RegExHelper.NameRegEx, suffix), "$");
  140. var rxsuffix = RegExHelper.GetCaptureGroup("suffix", suffix);
  141. NameTransformer.AddRule(
  142. String.Concat(nsSourceReplaceRegEx, String.Format(nameFormat, rxbase, rxsuffix), "$"),
  143. replist.ToArray(),
  144. rxsrcfilter
  145. );
  146. }
  147. /// <summary>
  148. /// Adds a standard type mapping based on namespace RegEx replace and filter patterns
  149. /// </summary>
  150. /// <param name="nsSourceReplaceRegEx">RegEx replace pattern for source namespace</param>
  151. /// <param name="nsSourceFilterRegEx">RegEx filter pattern for source namespace</param>
  152. /// <param name="nsTargetRegEx">RegEx replace value for target namespace</param>
  153. /// <param name="viewSuffix">Suffix for type name. Should be "View" or synonym of "View". (Optional)</param>
  154. public static void AddTypeMapping(string nsSourceReplaceRegEx, string nsSourceFilterRegEx, string nsTargetRegEx, string viewSuffix = "View") {
  155. AddTypeMapping(nsSourceReplaceRegEx, nsSourceFilterRegEx, new[] { nsTargetRegEx }, viewSuffix);
  156. }
  157. /// <summary>
  158. /// Adds a standard type mapping based on simple namespace mapping
  159. /// </summary>
  160. /// <param name="nsSource">Namespace of source type</param>
  161. /// <param name="nsTargets">Namespaces of target type as an array</param>
  162. /// <param name="viewSuffix">Suffix for type name. Should be "View" or synonym of "View". (Optional)</param>
  163. public static void AddNamespaceMapping(string nsSource, string[] nsTargets, string viewSuffix = "View") {
  164. //need to terminate with "." in order to concatenate with type name later
  165. var nsencoded = RegExHelper.NamespaceToRegEx(nsSource + ".");
  166. //Start pattern search from beginning of string ("^")
  167. //unless original string was blank (i.e. special case to indicate "append target to source")
  168. if (!String.IsNullOrEmpty(nsSource)) {
  169. nsencoded = "^" + nsencoded;
  170. }
  171. //Capture namespace as "origns" in case we need to use it in the output in the future
  172. var nsreplace = RegExHelper.GetCaptureGroup("origns", nsencoded);
  173. var nsTargetsRegEx = nsTargets.Select(t => t + ".").ToArray();
  174. AddTypeMapping(nsreplace, null, nsTargetsRegEx, viewSuffix);
  175. }
  176. /// <summary>
  177. /// Adds a standard type mapping based on simple namespace mapping
  178. /// </summary>
  179. /// <param name="nsSource">Namespace of source type</param>
  180. /// <param name="nsTarget">Namespace of target type</param>
  181. /// <param name="viewSuffix">Suffix for type name. Should be "View" or synonym of "View". (Optional)</param>
  182. public static void AddNamespaceMapping(string nsSource, string nsTarget, string viewSuffix = "View") {
  183. AddNamespaceMapping(nsSource, new[] { nsTarget }, viewSuffix);
  184. }
  185. /// <summary>
  186. /// Adds a standard type mapping by substituting one subnamespace for another
  187. /// </summary>
  188. /// <param name="nsSource">Subnamespace of source type</param>
  189. /// <param name="nsTargets">Subnamespaces of target type as an array</param>
  190. /// <param name="viewSuffix">Suffix for type name. Should be "View" or synonym of "View". (Optional)</param>
  191. public static void AddSubNamespaceMapping(string nsSource, string[] nsTargets, string viewSuffix = "View") {
  192. //need to terminate with "." in order to concatenate with type name later
  193. var nsencoded = RegExHelper.NamespaceToRegEx(nsSource + ".");
  194. string rxbeforetgt, rxaftersrc, rxaftertgt;
  195. var rxbeforesrc = rxbeforetgt = rxaftersrc = rxaftertgt = String.Empty;
  196. if (!String.IsNullOrEmpty(nsSource))
  197. {
  198. if (!nsSource.StartsWith("*")) {
  199. rxbeforesrc = RegExHelper.GetNamespaceCaptureGroup("nsbefore");
  200. rxbeforetgt = @"${nsbefore}";
  201. }
  202. if (!nsSource.EndsWith("*")) {
  203. rxaftersrc = RegExHelper.GetNamespaceCaptureGroup("nsafter");
  204. rxaftertgt = "${nsafter}";
  205. }
  206. }
  207. var rxmid = RegExHelper.GetCaptureGroup("subns", nsencoded);
  208. var nsreplace = String.Concat(rxbeforesrc, rxmid, rxaftersrc);
  209. var nsTargetsRegEx = nsTargets.Select(t => String.Concat(rxbeforetgt, t, ".", rxaftertgt)).ToArray();
  210. AddTypeMapping(nsreplace, null, nsTargetsRegEx, viewSuffix);
  211. }
  212. /// <summary>
  213. /// Adds a standard type mapping by substituting one subnamespace for another
  214. /// </summary>
  215. /// <param name="nsSource">Subnamespace of source type</param>
  216. /// <param name="nsTarget">Subnamespace of target type</param>
  217. /// <param name="viewSuffix">Suffix for type name. Should be "View" or synonym of "View". (Optional)</param>
  218. public static void AddSubNamespaceMapping(string nsSource, string nsTarget, string viewSuffix = "View") {
  219. AddSubNamespaceMapping(nsSource, new[] { nsTarget }, viewSuffix);
  220. }
  221. /// <summary>
  222. /// Retrieves the view from the IoC container or tries to create it if not found.
  223. /// </summary>
  224. /// <remarks>
  225. /// Pass the type of view as a parameter and recieve an instance of the view.
  226. /// </remarks>
  227. public static Func<Type, UIElement> GetOrCreateViewType = viewType => {
  228. var view = IoC.GetAllInstances(viewType)
  229. .FirstOrDefault() as UIElement;
  230. if (view != null) {
  231. InitializeComponent(view);
  232. return view;
  233. }
  234. #if !WinRT && !XFORMS
  235. if(viewType.IsInterface || viewType.IsAbstract || !typeof(UIElement).IsAssignableFrom(viewType))
  236. return new TextBlock { Text = string.Format("Cannot create {0}.", viewType.FullName) };
  237. #else
  238. var viewTypeInfo = viewType.GetTypeInfo();
  239. var uiElementInfo = typeof(UIElement).GetTypeInfo();
  240. if (viewTypeInfo.IsInterface || viewTypeInfo.IsAbstract || !uiElementInfo.IsAssignableFrom(viewTypeInfo))
  241. return new TextBlock { Text = string.Format("Cannot create {0}.", viewType.FullName) };
  242. #endif
  243. view = (UIElement)System.Activator.CreateInstance(viewType);
  244. InitializeComponent(view);
  245. return view;
  246. };
  247. /// <summary>
  248. /// Modifies the name of the type to be used at design time.
  249. /// </summary>
  250. public static Func<string, string> ModifyModelTypeAtDesignTime = modelTypeName =>
  251. {
  252. if (modelTypeName.StartsWith("_")) {
  253. var index = modelTypeName.IndexOf('.');
  254. modelTypeName = modelTypeName.Substring(index + 1);
  255. index = modelTypeName.IndexOf('.');
  256. modelTypeName = modelTypeName.Substring(index + 1);
  257. }
  258. return modelTypeName;
  259. };
  260. /// <summary>
  261. /// Transforms a ViewModel type name into all of its possible View type names. Optionally accepts an instance
  262. /// of context object
  263. /// </summary>
  264. /// <returns>Enumeration of transformed names</returns>
  265. /// <remarks>Arguments:
  266. /// typeName = The name of the ViewModel type being resolved to its companion View.
  267. /// context = An instance of the context or null.
  268. /// </remarks>
  269. public static Func<string, object, IEnumerable<string>> TransformName = (typeName, context) => {
  270. Func<string, string> getReplaceString;
  271. if (context == null) {
  272. getReplaceString = r => r;
  273. return NameTransformer.Transform(typeName, getReplaceString);
  274. }
  275. var contextstr = ContextSeparator + context;
  276. string grpsuffix = String.Empty;
  277. if (useNameSuffixesInMappings) {
  278. //Create RegEx for matching any of the synonyms registered
  279. var synonymregex = "(" + String.Join("|", ViewSuffixList.ToArray()) + ")";
  280. grpsuffix = RegExHelper.GetCaptureGroup("suffix", synonymregex);
  281. }
  282. const string grpbase = @"\${basename}";
  283. var patternregex = String.Format(nameFormat, grpbase, grpsuffix) + "$";
  284. //Strip out any synonym by just using contents of base capture group with context string
  285. var replaceregex = "${basename}" + contextstr;
  286. //Strip out the synonym
  287. getReplaceString = r => Regex.Replace(r, patternregex, replaceregex);
  288. //Return only the names for the context
  289. return NameTransformer.Transform(typeName, getReplaceString).Where(n => n.EndsWith(contextstr));
  290. };
  291. /// <summary>
  292. /// Locates the view type based on the specified model type.
  293. /// </summary>
  294. /// <returns>The view.</returns>
  295. /// <remarks>
  296. /// Pass the model type, display location (or null) and the context instance (or null) as parameters and receive a view type.
  297. /// </remarks>
  298. public static Func<Type, DependencyObject, object, Type> LocateTypeForModelType = (modelType, displayLocation, context) => {
  299. var viewTypeName = modelType.FullName;
  300. if (View.InDesignMode) {
  301. viewTypeName = ModifyModelTypeAtDesignTime(viewTypeName);
  302. }
  303. viewTypeName = viewTypeName.Substring(
  304. 0,
  305. viewTypeName.IndexOf('`') < 0
  306. ? viewTypeName.Length
  307. : viewTypeName.IndexOf('`')
  308. );
  309. var viewTypeList = TransformName(viewTypeName, context);
  310. var viewType = AssemblySource.FindTypeByNames(viewTypeList);
  311. if (viewType == null) {
  312. Log.Warn("View not found. Searched: {0}.", string.Join(", ", viewTypeList.ToArray()));
  313. }
  314. return viewType;
  315. };
  316. /// <summary>
  317. /// Locates the view for the specified model type.
  318. /// </summary>
  319. /// <returns>The view.</returns>
  320. /// <remarks>
  321. /// Pass the model type, display location (or null) and the context instance (or null) as parameters and receive a view instance.
  322. /// </remarks>
  323. public static Func<Type, DependencyObject, object, UIElement> LocateForModelType = (modelType, displayLocation, context) => {
  324. var viewType = LocateTypeForModelType(modelType, displayLocation, context);
  325. return viewType == null
  326. ? new TextBlock { Text = string.Format("Cannot find view for {0}.", modelType) }
  327. : GetOrCreateViewType(viewType);
  328. };
  329. /// <summary>
  330. /// Locates the view for the specified model instance.
  331. /// </summary>
  332. /// <returns>The view.</returns>
  333. /// <remarks>
  334. /// Pass the model instance, display location (or null) and the context (or null) as parameters and receive a view instance.
  335. /// </remarks>
  336. public static Func<object, DependencyObject, object, UIElement> LocateForModel = (model, displayLocation, context) => {
  337. var viewAware = model as IViewAware;
  338. if (viewAware != null) {
  339. var view = viewAware.GetView(context) as UIElement;
  340. if (view != null) {
  341. #if !SILVERLIGHT && !WinRT && !XFORMS
  342. var windowCheck = view as Window;
  343. if (windowCheck == null || (!windowCheck.IsLoaded && !(new WindowInteropHelper(windowCheck).Handle == IntPtr.Zero))) {
  344. Log.Info("Using cached view for {0}.", model);
  345. return view;
  346. }
  347. #else
  348. Log.Info("Using cached view for {0}.", model);
  349. return view;
  350. #endif
  351. }
  352. }
  353. return LocateForModelType(model.GetType(), displayLocation, context);
  354. };
  355. /// <summary>
  356. /// Transforms a view type into a pack uri.
  357. /// </summary>
  358. public static Func<Type, Type, string> DeterminePackUriFromType = (viewModelType, viewType) => {
  359. #if !WinRT && !XFORMS
  360. var assemblyName = viewType.Assembly.GetAssemblyName();
  361. var applicationAssemblyName = Application.Current.GetType().Assembly.GetAssemblyName();
  362. #else
  363. var assemblyName = viewType.GetTypeInfo().Assembly.GetAssemblyName();
  364. var applicationAssemblyName = Application.Current.GetType().GetTypeInfo().Assembly.GetAssemblyName();
  365. #endif
  366. var viewTypeName = viewType.FullName;
  367. if (viewTypeName.StartsWith(assemblyName))
  368. viewTypeName = viewTypeName.Substring(assemblyName.Length);
  369. var uri = viewTypeName.Replace(".", "/") + ".xaml";
  370. if(!applicationAssemblyName.Equals(assemblyName)) {
  371. return "/" + assemblyName + ";component" + uri;
  372. }
  373. return uri;
  374. };
  375. /// <summary>
  376. /// When a view does not contain a code-behind file, we need to automatically call InitializeCompoent.
  377. /// </summary>
  378. /// <param name = "element">The element to initialize</param>
  379. public static void InitializeComponent(object element) {
  380. #if XFORMS
  381. return;
  382. #elif !WinRT
  383. var method = element.GetType()
  384. .GetMethod("InitializeComponent", BindingFlags.Public | BindingFlags.Instance);
  385. method.Invoke(element, null);
  386. #else
  387. var method = element.GetType().GetTypeInfo()
  388. .GetDeclaredMethods("InitializeComponent")
  389. .SingleOrDefault(m => m.GetParameters().Length == 0);
  390. method?.Invoke(element, null);
  391. #endif
  392. }
  393. }
  394. }