[NUI] TCSACR-226 code change (#1032)
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI / src / public / XamlBinding / ResourceDictionary.cs
1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using System.Collections.ObjectModel;
5 using System.Collections.Specialized;
6 using System.ComponentModel;
7 using System.Globalization;
8 using System.Linq;
9 using System.Reflection;
10 using System.Runtime.CompilerServices;
11
12 using Tizen.NUI.Binding.Internals;
13 using Tizen.NUI.Xaml;
14
15 namespace Tizen.NUI.Binding
16 {
17     /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
18     [EditorBrowsable(EditorBrowsableState.Never)]
19     public class ResourceDictionary : IResourceDictionary, IDictionary<string, object>
20     {
21         static ConditionalWeakTable<Type, ResourceDictionary> s_instances = new ConditionalWeakTable<Type, ResourceDictionary>();
22         readonly Dictionary<string, object> _innerDictionary = new Dictionary<string, object>();
23         ResourceDictionary _mergedInstance;
24         Type _mergedWith;
25         Uri _source;
26
27         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
28         [EditorBrowsable(EditorBrowsableState.Never)]
29         public ResourceDictionary()
30         {
31             DependencyService.Register<IResourcesLoader, ResourcesLoader>();
32         }
33
34         /// <summary>
35         /// Gets or sets the type of object with which the resource dictionary is merged.
36         /// </summary>
37         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
38         [EditorBrowsable(EditorBrowsableState.Never)]
39         [TypeConverter(typeof(TypeTypeConverter))]
40         [Obsolete("Use Source")]
41         public Type MergedWith {
42             get { return _mergedWith; }
43             set {
44                 if (_mergedWith == value)
45                     return;
46
47                 if (_source != null)
48                     throw new ArgumentException("MergedWith can not be used with Source");
49
50                 if (!typeof(ResourceDictionary).GetTypeInfo().IsAssignableFrom(value.GetTypeInfo()))
51                     throw new ArgumentException("MergedWith should inherit from ResourceDictionary");
52
53                 _mergedWith = value;
54                 if (_mergedWith == null)
55                     return;
56
57                 _mergedInstance = s_instances.GetValue(_mergedWith, (key) => (ResourceDictionary)Activator.CreateInstance(key));
58                 OnValuesChanged(_mergedInstance.ToArray());
59             }
60         }
61
62         /// <summary>
63         /// Gets or sets the URI of the merged resource dictionary.
64         /// </summary>
65         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
66         [EditorBrowsable(EditorBrowsableState.Never)]
67         [TypeConverter(typeof(RDSourceTypeConverter))]
68         public Uri Source {
69             get { return _source; }
70             set {
71                 if (_source == value)
72                     return;
73                 throw new InvalidOperationException("Source can only be set from XAML."); //through the RDSourceTypeConverter
74             }
75         }
76
77         /// <summary>
78         /// To set and load source.
79         /// </summary>
80         /// <param name="value">The source.</param>
81         /// <param name="resourcePath">The resource path.</param>
82         /// <param name="assembly">The assembly.</param>
83         /// <param name="lineInfo">The xml line info.</param>
84         /// Used by the XamlC compiled converter.
85         [EditorBrowsable(EditorBrowsableState.Never)]
86         public void SetAndLoadSource(Uri value, string resourcePath, Assembly assembly, System.Xml.IXmlLineInfo lineInfo)
87         {
88             _source = value;
89             if (_mergedWith != null)
90                 throw new ArgumentException("Source can not be used with MergedWith");
91
92             //this will return a type if the RD as an x:Class element, and codebehind
93             var type = XamlResourceIdAttribute.GetTypeForPath(assembly, resourcePath);
94             if (type != null)
95                 _mergedInstance = s_instances.GetValue(type, (key) => (ResourceDictionary)Activator.CreateInstance(key));
96             else
97                 _mergedInstance = DependencyService.Get<IResourcesLoader>()?.CreateFromResource<ResourceDictionary>(resourcePath, assembly, lineInfo);
98             OnValuesChanged(_mergedInstance.ToArray());
99         }
100
101         ICollection<ResourceDictionary> _mergedDictionaries;
102
103         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
104         [EditorBrowsable(EditorBrowsableState.Never)]
105         public ICollection<ResourceDictionary> MergedDictionaries {
106             get {
107                 if (_mergedDictionaries == null) {
108                     var col = new ObservableCollection<ResourceDictionary>();
109                     col.CollectionChanged += MergedDictionaries_CollectionChanged;
110                     _mergedDictionaries = col;
111                 }
112                 return _mergedDictionaries;
113             }
114         }
115
116         internal IList<StyleSheets.StyleSheet> StyleSheets { get; set; }
117
118         void StyleSheetsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
119         {
120             switch (e.Action) {
121             case NotifyCollectionChangedAction.Add:
122                     ValuesChanged?.Invoke(this, ResourcesChangedEventArgs.StyleSheets);
123                 break;
124             }
125         }
126         IList<ResourceDictionary> _collectionTrack;
127
128         void MergedDictionaries_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
129         {
130             // Move() isn't exposed by ICollection
131             if (e.Action == NotifyCollectionChangedAction.Move)
132                 return;
133
134             _collectionTrack = _collectionTrack ?? new List<ResourceDictionary>();
135             // Collection has been cleared
136             if (e.Action == NotifyCollectionChangedAction.Reset) {
137                 foreach (var dictionary in _collectionTrack)
138                     dictionary.ValuesChanged -= Item_ValuesChanged;
139
140                 _collectionTrack.Clear();
141                 return;
142             }
143
144             // New Items
145             if (e.NewItems != null)
146             {
147                 foreach (var item in e.NewItems)
148                 {
149                     var rd = (ResourceDictionary)item;
150                     _collectionTrack.Add(rd);
151                     rd.ValuesChanged += Item_ValuesChanged;
152                     OnValuesChanged(rd.ToArray());
153                 }
154             }
155
156             // Old Items
157             if (e.OldItems != null)
158             {
159                 foreach (var item in e.OldItems)
160                 {
161                     var rd = (ResourceDictionary)item;
162                     rd.ValuesChanged -= Item_ValuesChanged;
163                     _collectionTrack.Remove(rd);
164                 }
165             }
166         }
167
168         void Item_ValuesChanged(object sender, ResourcesChangedEventArgs e)
169         {
170             OnValuesChanged(e.Values.ToArray());
171         }
172
173         void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
174         {
175             ((ICollection<KeyValuePair<string, object>>)_innerDictionary).Add(item);
176             OnValuesChanged(item);
177         }
178
179         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
180         [EditorBrowsable(EditorBrowsableState.Never)]
181         public void Clear()
182         {
183             _innerDictionary.Clear();
184         }
185
186         bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
187         {
188             return ((ICollection<KeyValuePair<string, object>>)_innerDictionary).Contains(item)
189                 || (_mergedInstance != null && _mergedInstance.Contains(item));
190         }
191
192         void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
193         {
194             ((ICollection<KeyValuePair<string, object>>)_innerDictionary).CopyTo(array, arrayIndex);
195         }
196
197         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
198         [EditorBrowsable(EditorBrowsableState.Never)]
199         public int Count
200         {
201             get { return _innerDictionary.Count; }
202         }
203
204         bool ICollection<KeyValuePair<string, object>>.IsReadOnly
205         {
206             get { return ((ICollection<KeyValuePair<string, object>>)_innerDictionary).IsReadOnly; }
207         }
208
209         bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
210         {
211             return ((ICollection<KeyValuePair<string, object>>)_innerDictionary).Remove(item);
212         }
213
214         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
215         [EditorBrowsable(EditorBrowsableState.Never)]
216         public void Add(string key, object value)
217         {
218             if (ContainsKey(key))
219                 throw new ArgumentException($"A resource with the key '{key}' is already present in the ResourceDictionary.");
220             _innerDictionary.Add(key, value);
221             OnValueChanged(key, value);
222         }
223
224         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
225         [EditorBrowsable(EditorBrowsableState.Never)]
226         public bool ContainsKey(string key)
227         {
228             return _innerDictionary.ContainsKey(key);
229         }
230
231         /// <summary>
232         /// Gets or sets the value according to index.
233         /// </summary>
234         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
235         [EditorBrowsable(EditorBrowsableState.Never)]
236         [IndexerName("Item")]
237         public object this[string index]
238         {
239             get
240             {
241                 if (_innerDictionary.ContainsKey(index))
242                     return _innerDictionary[index];
243                 if (_mergedInstance != null && _mergedInstance.ContainsKey(index))
244                     return _mergedInstance[index];
245                 if (MergedDictionaries != null)
246                     foreach (var dict in MergedDictionaries.Reverse())
247                         if (dict.ContainsKey(index))
248                             return dict[index];
249                 throw new KeyNotFoundException($"The resource '{index}' is not present in the dictionary.");
250             }
251             set
252             {
253                 _innerDictionary[index] = value;
254                 OnValueChanged(index, value);
255             }
256         }
257
258         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
259         [EditorBrowsable(EditorBrowsableState.Never)]
260         public ICollection<string> Keys
261         {
262             get { return _innerDictionary.Keys; }
263         }
264
265         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
266         [EditorBrowsable(EditorBrowsableState.Never)]
267         public bool Remove(string key)
268         {
269             return _innerDictionary.Remove(key);
270         }
271
272         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
273         [EditorBrowsable(EditorBrowsableState.Never)]
274         public ICollection<object> Values
275         {
276             get { return _innerDictionary.Values; }
277         }
278
279         IEnumerator IEnumerable.GetEnumerator()
280         {
281             return GetEnumerator();
282         }
283
284         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
285         [EditorBrowsable(EditorBrowsableState.Never)]
286         public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
287         {
288             return _innerDictionary.GetEnumerator();
289         }
290
291         internal IEnumerable<KeyValuePair<string, object>> MergedResources {
292             get {
293                 if (MergedDictionaries != null)
294                     foreach (var r in MergedDictionaries.Reverse().SelectMany(x => x.MergedResources))
295                         yield return r;
296                 if (_mergedInstance != null)
297                     foreach (var r in _mergedInstance.MergedResources)
298                         yield return r;
299                 foreach (var r in _innerDictionary)
300                     yield return r;
301             }
302         }
303
304         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
305         [EditorBrowsable(EditorBrowsableState.Never)]
306         public bool TryGetValue(string key, out object value)
307         {
308             return _innerDictionary.TryGetValue(key, out value)
309                 || (_mergedInstance != null && _mergedInstance.TryGetValue(key, out value))
310                 || (MergedDictionaries != null && TryGetMergedDictionaryValue(key, out value));
311         }
312
313         bool TryGetMergedDictionaryValue(string key, out object value)
314         {
315             foreach (var dictionary in MergedDictionaries.Reverse())
316                 if (dictionary.TryGetValue(key, out value))
317                     return true;
318
319             value = null;
320             return false;
321         }
322
323         event EventHandler<ResourcesChangedEventArgs> IResourceDictionary.ValuesChanged
324         {
325             add { ValuesChanged += value; }
326             remove { ValuesChanged -= value; }
327         }
328
329         internal void Add(Style style)
330         {
331             if (string.IsNullOrEmpty(style.Class))
332                 Add(style.TargetType.FullName, style);
333             else
334             {
335                 IList<Style> classes;
336                 object outclasses;
337                 if (!TryGetValue(Style.StyleClassPrefix + style.Class, out outclasses) || (classes = outclasses as IList<Style>) == null)
338                     classes = new List<Style>();
339                 classes.Add(style);
340                 this[Style.StyleClassPrefix + style.Class] = classes;
341             }
342         }
343
344         /// This will be public opened in tizen_5.0 after ACR done. Before ACR, need to be hidden as inhouse API.
345         [EditorBrowsable(EditorBrowsableState.Never)]
346         public void Add(ResourceDictionary mergedResourceDictionary)
347         {
348             MergedDictionaries.Add(mergedResourceDictionary);
349         }
350
351         internal void Add(StyleSheets.StyleSheet styleSheet)
352         {
353             StyleSheets = StyleSheets ?? new List<StyleSheets.StyleSheet>(2);
354             StyleSheets.Add(styleSheet);
355             ValuesChanged?.Invoke(this, ResourcesChangedEventArgs.StyleSheets);
356         }
357
358         void OnValueChanged(string key, object value)
359         {
360             OnValuesChanged(new KeyValuePair<string, object>(key, value));
361         }
362
363         void OnValuesChanged(params KeyValuePair<string, object>[] values)
364         {
365             if (values == null || values.Length == 0)
366                 return;
367             ValuesChanged?.Invoke(this, new ResourcesChangedEventArgs(values));
368         }
369
370         event EventHandler<ResourcesChangedEventArgs> ValuesChanged;
371
372         [Xaml.ProvideCompiled("Tizen.NUI.Xaml.Core.XamlC.RDSourceTypeConverter")]
373         internal class RDSourceTypeConverter : TypeConverter, IExtendedTypeConverter
374         {
375             object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
376             {
377                 if (serviceProvider == null)
378                     throw new ArgumentNullException(nameof(serviceProvider));
379
380                 var targetRD = (serviceProvider.GetService(typeof(Xaml.IProvideValueTarget)) as Xaml.IProvideValueTarget)?.TargetObject as ResourceDictionary;
381                 if (targetRD == null)
382                     return null;
383
384                 var rootObjectType = (serviceProvider.GetService(typeof(Xaml.IRootObjectProvider)) as Xaml.IRootObjectProvider)?.RootObject.GetType();
385                 if (rootObjectType == null)
386                     return null;
387
388                 var lineInfo = (serviceProvider.GetService(typeof(Xaml.IXmlLineInfoProvider)) as Xaml.IXmlLineInfoProvider)?.XmlLineInfo;
389                 var rootTargetPath = XamlResourceIdAttribute.GetPathForType(rootObjectType);
390                 var uri = new Uri(value, UriKind.Relative); //we don't want file:// uris, even if they start with '/'
391                 var resourcePath = GetResourcePath(uri, rootTargetPath);
392
393                 targetRD.SetAndLoadSource(uri, resourcePath, rootObjectType.GetTypeInfo().Assembly, lineInfo);
394                 return uri;
395             }
396
397             internal static string GetResourcePath(Uri uri, string rootTargetPath)
398             {
399                 //need a fake scheme so it's not seen as file:// uri, and the forward slashes are valid on all plats
400                 var resourceUri = uri.OriginalString.StartsWith("/", StringComparison.Ordinal)
401                                      ? new Uri($"pack://{uri.OriginalString}", UriKind.Absolute)
402                                      : new Uri($"pack:///{rootTargetPath}/../{uri.OriginalString}", UriKind.Absolute);
403
404                 //drop the leading '/'
405                 return resourceUri.AbsolutePath.Substring(1);
406             }
407
408             object IExtendedTypeConverter.ConvertFrom(CultureInfo culture, object value, IServiceProvider serviceProvider)
409             {
410                 throw new NotImplementedException();
411             }
412
413             public override object ConvertFromInvariantString(string value)
414             {
415                 throw new NotImplementedException();
416             }
417         }
418     }
419 }