1 //------------------------------------------------------------------------------
2 // <copyright file="ScriptComponentDescriptor.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //------------------------------------------------------------------------------
6 
7 namespace System.Web.UI {
8     using System;
9     using System.Collections.Generic;
10     using System.Collections.ObjectModel;
11     using System.Diagnostics;
12     using System.Diagnostics.CodeAnalysis;
13     using System.Globalization;
14     using System.Text;
15     using System.Text.RegularExpressions;
16     using System.Web;
17     using System.Web.Resources;
18     using System.Web.Script.Serialization;
19 
20     public class ScriptComponentDescriptor : ScriptDescriptor {
21         // PERF: In the ScriptControl/Properties/Scenario.aspx perf test, SortedList is 2% faster than
22         // SortedDictionary.
23         private string _elementIDInternal;
24         private SortedList<string, string> _events;
25         private string _id;
26         private SortedList<string, Expression> _properties;
27         private bool _registerDispose = true;
28         private JavaScriptSerializer _serializer;
29         private string _type;
30 
ScriptComponentDescriptor(string type)31         public ScriptComponentDescriptor(string type) {
32             if (String.IsNullOrEmpty(type)) {
33                 throw new ArgumentException(AtlasWeb.Common_NullOrEmpty, "type");
34             }
35             _type = type;
36         }
37 
38         // Used by ScriptBehaviorDescriptor and ScriptControlDesriptor
ScriptComponentDescriptor(string type, string elementID)39         internal ScriptComponentDescriptor(string type, string elementID)
40             : this(type) {
41             if (String.IsNullOrEmpty(elementID)) {
42                 throw new ArgumentException(AtlasWeb.Common_NullOrEmpty, "elementID");
43             }
44             _elementIDInternal = elementID;
45         }
46 
47         [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ID")]
48         public virtual string ClientID {
49             get {
50                 return ID;
51             }
52         }
53 
54         internal string ElementIDInternal {
55             get {
56                 return _elementIDInternal;
57             }
58         }
59 
60         private SortedList<string, string> Events {
61             get {
62                 if (_events == null) {
63                     _events = new SortedList<string, string>(StringComparer.Ordinal);
64                 }
65                 return _events;
66             }
67         }
68 
69         [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ID")]
70         public virtual string ID {
71             get {
72                 return _id ?? String.Empty;
73             }
74             set {
75                 _id = value;
76             }
77         }
78 
79         private SortedList<string, Expression> Properties {
80             get {
81                 if (_properties == null) {
82                     _properties = new SortedList<string, Expression>(StringComparer.Ordinal);
83                 }
84                 return _properties;
85             }
86         }
87 
88         internal bool RegisterDispose {
89             get {
90                 return _registerDispose;
91             }
92             set {
93                 _registerDispose = value;
94             }
95         }
96 
97         private JavaScriptSerializer Serializer {
98             get {
99                 if (_serializer == null) {
100                     _serializer = new JavaScriptSerializer();
101                 }
102                 return _serializer;
103             }
104         }
105 
106         [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods",
107             Justification = "Refers to a script element, not to Object.GetType()")]
108         public string Type {
109             get {
110                 return _type;
111             }
112             set {
113                 if (String.IsNullOrEmpty(value)) {
114                     throw new ArgumentException(AtlasWeb.Common_NullOrEmpty, "value");
115                 }
116                 _type = value;
117             }
118         }
119 
120         [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ID")]
AddComponentProperty(string name, string componentID)121         public void AddComponentProperty(string name, string componentID) {
122             if (String.IsNullOrEmpty(componentID)) {
123                 throw new ArgumentException(AtlasWeb.Common_NullOrEmpty, "componentID");
124             }
125             AddProperty(name, new ComponentReference(componentID));
126         }
127 
128         [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ID")]
AddElementProperty(string name, string elementID)129         public void AddElementProperty(string name, string elementID) {
130             if (String.IsNullOrEmpty(elementID)) {
131                 throw new ArgumentException(AtlasWeb.Common_NullOrEmpty, "elementID");
132             }
133             AddProperty(name, new ElementReference(elementID));
134         }
135 
AddEvent(string name, string handler)136         public void AddEvent(string name, string handler) {
137             if (String.IsNullOrEmpty(name)) {
138                 throw new ArgumentException(AtlasWeb.Common_NullOrEmpty, "name");
139             }
140             if (String.IsNullOrEmpty(handler)) {
141                 throw new ArgumentException(AtlasWeb.Common_NullOrEmpty, "handler");
142             }
143             Events[name] = handler;
144         }
145 
AddProperty(string name, object value)146         public void AddProperty(string name, object value) {
147             AddProperty(name, new ObjectReference(value));
148         }
149 
AddProperty(string name, Expression value)150         private void AddProperty(string name, Expression value) {
151             if (String.IsNullOrEmpty(name)) {
152                 throw new ArgumentException(AtlasWeb.Common_NullOrEmpty, "name");
153             }
154             Debug.Assert(value != null);
155             Properties[name] = value;
156         }
157 
AddScriptProperty(string name, string script)158         public void AddScriptProperty(string name, string script) {
159             if (String.IsNullOrEmpty(script)) {
160                 throw new ArgumentException(AtlasWeb.Common_NullOrEmpty, "script");
161             }
162             AddProperty(name, new ScriptExpression(script));
163         }
164 
AppendEventsScript(StringBuilder builder)165         private void AppendEventsScript(StringBuilder builder) {
166             // PERF: Use field directly to avoid creating Dictionary if not already created
167             if (_events != null && _events.Count > 0) {
168                 builder.Append('{');
169                 bool first = true;
170 
171                 // Can't use JavaScriptSerializer directly on events dictionary, since the values are
172                 // JavaScript functions and should not be quoted.
173                 foreach (KeyValuePair<string, string> e in _events) {
174                     if (first) {
175                         first = false;
176                     }
177                     else {
178                         builder.Append(',');
179                     }
180                     builder.Append('"');
181                     builder.Append(HttpUtility.JavaScriptStringEncode(e.Key));
182                     builder.Append('"');
183                     builder.Append(':');
184                     builder.Append(e.Value);
185                 }
186 
187                 builder.Append("}");
188             }
189             else {
190                 builder.Append("null");
191             }
192         }
193 
AppendPropertiesScript(StringBuilder builder)194         private void AppendPropertiesScript(StringBuilder builder) {
195             bool first = true;
196 
197             // PERF: Use field directly to avoid creating Dictionary if not already created
198             if (_properties != null && _properties.Count > 0) {
199                 foreach (KeyValuePair<string, Expression> p in _properties) {
200                     if (p.Value.Type == ExpressionType.Script) {
201                         if (first) {
202                             builder.Append("{");
203                             first = false;
204                         }
205                         else {
206                             builder.Append(",");
207                         }
208                         builder.Append('"');
209                         builder.Append(HttpUtility.JavaScriptStringEncode(p.Key));
210                         builder.Append('"');
211                         builder.Append(':');
212                         p.Value.AppendValue(Serializer, builder);
213                     }
214                 }
215             }
216 
217             if (first) {
218                 // If we didn't see any properties, append "null"
219                 builder.Append("null");
220             }
221             else {
222                 // Else, close the JSON object
223                 builder.Append("}");
224             }
225         }
226 
AppendReferencesScript(StringBuilder builder)227         private void AppendReferencesScript(StringBuilder builder) {
228             bool first = true;
229 
230             // PERF: Use field directly to avoid creating Dictionary if not already created
231             if (_properties != null && _properties.Count > 0) {
232                 foreach (KeyValuePair<string, Expression> p in _properties) {
233                     if (p.Value.Type == ExpressionType.ComponentReference) {
234                         if (first) {
235                             builder.Append("{");
236                             first = false;
237                         }
238                         else {
239                             builder.Append(",");
240                         }
241                         builder.Append('"');
242                         builder.Append(HttpUtility.JavaScriptStringEncode(p.Key));
243                         builder.Append('"');
244                         builder.Append(':');
245                         p.Value.AppendValue(Serializer, builder);
246                     }
247                 }
248             }
249 
250             if (first) {
251                 // If we didn't see any references, append "null"
252                 builder.Append("null");
253             }
254             else {
255                 // Else, close the JSON object
256                 builder.Append("}");
257             }
258         }
259 
GetScript()260         protected internal override string GetScript() {
261             const string separator = ", ";
262 
263             if (!String.IsNullOrEmpty(ID)) {
264                 AddProperty("id", ID);
265             }
266 
267             StringBuilder builder = new StringBuilder();
268             builder.Append("$create(");
269 
270             builder.Append(Type);
271 
272             builder.Append(separator);
273             AppendPropertiesScript(builder);
274 
275             builder.Append(separator);
276             AppendEventsScript(builder);
277 
278             builder.Append(separator);
279             AppendReferencesScript(builder);
280 
281             if (ElementIDInternal != null) {
282                 builder.Append(separator);
283                 builder.Append("$get(\"");
284                 builder.Append(HttpUtility.JavaScriptStringEncode(ElementIDInternal));
285                 builder.Append("\")");
286             }
287 
288             builder.Append(");");
289 
290             return builder.ToString();
291         }
292 
RegisterDisposeForDescriptor(ScriptManager scriptManager, Control owner)293         internal override void RegisterDisposeForDescriptor(ScriptManager scriptManager, Control owner) {
294             if (RegisterDispose && scriptManager.SupportsPartialRendering) {
295                 // If partial rendering is supported, register a JavaScript statement
296                 // that will dispose this component if the UpdatePanel it is inside is
297                 // getting refreshed. Only components need this; controls are associated
298                 // with DOM elements so they get disposed through their 'dispose' expando.
299                 scriptManager.RegisterDispose(owner, "$find('" + ID + "').dispose();");
300             }
301         }
302 
303         private abstract class Expression {
304             public abstract ExpressionType Type { get; }
AppendValue(JavaScriptSerializer serializer, StringBuilder builder)305             public abstract void AppendValue(JavaScriptSerializer serializer, StringBuilder builder);
306         }
307 
308         private enum ExpressionType {
309             Script,
310             ComponentReference
311         }
312 
313         private sealed class ComponentReference : Expression {
314             private string _componentID;
315 
ComponentReference(string componentID)316             public ComponentReference(string componentID) {
317                 _componentID = componentID;
318             }
319 
320             public override ExpressionType Type {
321                 get {
322                     return ExpressionType.ComponentReference;
323                 }
324             }
325 
AppendValue(JavaScriptSerializer serializer, StringBuilder builder)326             public override void AppendValue(JavaScriptSerializer serializer, StringBuilder builder) {
327                 builder.Append('"');
328                 builder.Append(HttpUtility.JavaScriptStringEncode(_componentID));
329                 builder.Append('"');
330             }
331         }
332 
333         private sealed class ElementReference : Expression {
334             private string _elementID;
335 
ElementReference(string elementID)336             public ElementReference(string elementID) {
337                 _elementID = elementID;
338             }
339 
340             public override ExpressionType Type {
341                 get {
342                     return ExpressionType.Script;
343                 }
344             }
345 
AppendValue(JavaScriptSerializer serializer, StringBuilder builder)346             public override void AppendValue(JavaScriptSerializer serializer, StringBuilder builder) {
347                 builder.Append("$get(\"");
348                 builder.Append(HttpUtility.JavaScriptStringEncode(_elementID));
349                 builder.Append("\")");
350             }
351         }
352 
353         private sealed class ObjectReference : Expression {
354             private object _value;
355 
ObjectReference(object value)356             public ObjectReference(object value) {
357                 _value = value;
358             }
359 
360             public override ExpressionType Type {
361                 get {
362                     return ExpressionType.Script;
363                 }
364             }
365 
AppendValue(JavaScriptSerializer serializer, StringBuilder builder)366             public override void AppendValue(JavaScriptSerializer serializer, StringBuilder builder) {
367                 // DevDiv Bugs 96574: pass SerializationFormat.JavaScript to serialize to straight JavaScript,
368                 // so date properties are rendered as dates
369                 serializer.Serialize(_value, builder, JavaScriptSerializer.SerializationFormat.JavaScript);
370             }
371         }
372 
373         private sealed class ScriptExpression : Expression {
374             private string _script;
375 
ScriptExpression(string script)376             public ScriptExpression(string script) {
377                 _script = script;
378             }
379 
380             public override ExpressionType Type {
381                 get {
382                     return ExpressionType.Script;
383                 }
384             }
385 
AppendValue(JavaScriptSerializer serializer, StringBuilder builder)386             public override void AppendValue(JavaScriptSerializer serializer, StringBuilder builder) {
387                 builder.Append(_script);
388             }
389         }
390     }
391 }
392