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