1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. 2 3 using System.Collections.Generic; 4 using System.Diagnostics.CodeAnalysis; 5 using System.Globalization; 6 using System.IO; 7 using System.Text; 8 using System.Web.WebPages.Resources; 9 using Microsoft.Internal.Web.Utils; 10 11 namespace System.Web.WebPages 12 { 13 // TODO(elipton): Clean this up and remove the suppression 14 [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "This is temporary (elipton)")] 15 public abstract class WebPageBase : WebPageRenderingBase 16 { 17 // Keep track of which sections RenderSection has already been called on 18 private HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase); 19 // Keep track of whether RenderBody has been called 20 private bool _renderedBody = false; 21 // Action for rendering the body within a layout page 22 private Action<TextWriter> _body; 23 24 // TODO(elipton): Figure out if we still need these two writers 25 private TextWriter _tempWriter; 26 private TextWriter _currentWriter; 27 28 private DynamicPageDataDictionary<dynamic> _dynamicPageData; 29 30 public override string Layout { get; set; } 31 32 public TextWriter Output 33 { 34 get { return OutputStack.Peek(); } 35 } 36 37 public Stack<TextWriter> OutputStack 38 { 39 get { return PageContext.OutputStack; } 40 } 41 42 public override IDictionary<object, dynamic> PageData 43 { 44 get { return PageContext.PageData; } 45 } 46 47 public override dynamic Page 48 { 49 get 50 { 51 if (_dynamicPageData == null) 52 { 53 _dynamicPageData = new DynamicPageDataDictionary<dynamic>((PageDataDictionary<dynamic>)PageData); 54 } 55 return _dynamicPageData; 56 } 57 } 58 59 // Retrieves the sections defined in the calling page. If this is null, that means 60 // this page has been requested directly. 61 private Dictionary<string, SectionWriter> PreviousSectionWriters 62 { 63 get 64 { 65 var top = SectionWritersStack.Pop(); 66 var previous = SectionWritersStack.Count > 0 ? SectionWritersStack.Peek() : null; 67 SectionWritersStack.Push(top); 68 return previous; 69 } 70 } 71 72 // Retrieves the current Dictionary of sectionWriters on the stack without poping it. 73 // There should be at least one on the stack which is added when the Render(ViewData,TextWriter) 74 // is called. 75 private Dictionary<string, SectionWriter> SectionWriters 76 { 77 get { return SectionWritersStack.Peek(); } 78 } 79 80 private Stack<Dictionary<string, SectionWriter>> SectionWritersStack 81 { 82 get { return PageContext.SectionWritersStack; } 83 } 84 ConfigurePage(WebPageBase parentPage)85 protected virtual void ConfigurePage(WebPageBase parentPage) 86 { 87 } 88 CreateInstanceFromVirtualPath(string virtualPath)89 public static WebPageBase CreateInstanceFromVirtualPath(string virtualPath) 90 { 91 return CreateInstanceFromVirtualPath(virtualPath, VirtualPathFactoryManager.Instance); 92 } 93 CreateInstanceFromVirtualPath(string virtualPath, IVirtualPathFactory virtualPathFactory)94 internal static WebPageBase CreateInstanceFromVirtualPath(string virtualPath, IVirtualPathFactory virtualPathFactory) 95 { 96 // Get the compiled object 97 try 98 { 99 WebPageBase webPage = virtualPathFactory.CreateInstance<WebPageBase>(virtualPath); 100 101 // Give it its virtual path 102 webPage.VirtualPath = virtualPath; 103 104 // Assign it the VirtualPathFactory 105 webPage.VirtualPathFactory = virtualPathFactory; 106 107 return webPage; 108 } 109 catch (HttpException e) 110 { 111 BuildManagerExceptionUtil.ThrowIfUnsupportedExtension(virtualPath, e); 112 throw; 113 } 114 } 115 116 /// <summary> 117 /// Attempts to create a WebPageBase instance from a virtualPath and wraps complex compiler exceptions with simpler messages 118 /// </summary> CreatePageFromVirtualPath(string virtualPath, HttpContextBase httpContext, Func<string, bool> virtualPathExists, DisplayModeProvider displayModeProvider, IDisplayMode displayMode)119 private WebPageBase CreatePageFromVirtualPath(string virtualPath, HttpContextBase httpContext, Func<string, bool> virtualPathExists, DisplayModeProvider displayModeProvider, IDisplayMode displayMode) 120 { 121 try 122 { 123 DisplayInfo resolvedDisplayInfo = displayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, httpContext, virtualPathExists, displayMode); 124 125 if (resolvedDisplayInfo != null) 126 { 127 var webPage = VirtualPathFactory.CreateInstance<WebPageBase>(resolvedDisplayInfo.FilePath); 128 129 if (webPage != null) 130 { 131 // Give it its virtual path 132 webPage.VirtualPath = virtualPath; 133 webPage.VirtualPathFactory = VirtualPathFactory; 134 webPage.DisplayModeProvider = DisplayModeProvider; 135 136 return webPage; 137 } 138 } 139 } 140 catch (HttpException e) 141 { 142 // If the path uses an unregistered extension, such as Foo.txt, 143 // then an error regarding build providers will be thrown. 144 // Check if this is the case and throw a simpler error. 145 BuildManagerExceptionUtil.ThrowIfUnsupportedExtension(virtualPath, e); 146 147 // If the path uses an extension registered with codedom, such as Foo.js, 148 // then an unfriendly compilation error might get thrown by the underlying compiler. 149 // Check if this is the case and throw a simpler error. 150 BuildManagerExceptionUtil.ThrowIfCodeDomDefinedExtension(virtualPath, e); 151 152 // Rethrow any errors 153 throw; 154 } 155 // The page is missing, could not be compiled or is of an invalid type. 156 throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_InvalidPageType, virtualPath)); 157 } 158 CreatePageContextFromParameters(bool isLayoutPage, params object[] data)159 private WebPageContext CreatePageContextFromParameters(bool isLayoutPage, params object[] data) 160 { 161 object first = null; 162 if (data != null && data.Length > 0) 163 { 164 first = data[0]; 165 } 166 167 var pageData = PageDataDictionary<dynamic>.CreatePageDataFromParameters(PageData, data); 168 169 return WebPageContext.CreateNestedPageContext(PageContext, pageData, first, isLayoutPage); 170 } 171 DefineSection(string name, SectionWriter action)172 public void DefineSection(string name, SectionWriter action) 173 { 174 if (SectionWriters.ContainsKey(name)) 175 { 176 throw new HttpException(String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionAleadyDefined, name)); 177 } 178 SectionWriters[name] = action; 179 } 180 EnsurePageCanBeRequestedDirectly(string methodName)181 internal void EnsurePageCanBeRequestedDirectly(string methodName) 182 { 183 if (PreviousSectionWriters == null) 184 { 185 throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_CannotRequestDirectly, VirtualPath, methodName)); 186 } 187 } 188 ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer)189 public void ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer) 190 { 191 ExecutePageHierarchy(pageContext, writer, startPage: null); 192 } 193 194 // This method is only used by WebPageBase to allow passing in the view context and writer. ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage)195 public void ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage) 196 { 197 PushContext(pageContext, writer); 198 199 if (startPage != null) 200 { 201 if (startPage != this) 202 { 203 var startPageContext = WebPageContext.CreateNestedPageContext<object>(parentContext: pageContext, pageData: null, model: null, isLayoutPage: false); 204 startPageContext.Page = startPage; 205 startPage.PageContext = startPageContext; 206 } 207 startPage.ExecutePageHierarchy(); 208 } 209 else 210 { 211 ExecutePageHierarchy(); 212 } 213 PopContext(); 214 } 215 216 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We really don't care if SourceHeader fails, and we don't want it to fail any real requests ever")] ExecutePageHierarchy()217 public override void ExecutePageHierarchy() 218 { 219 // Unlike InitPages, for a WebPage there is no hierarchy - it is always 220 // the last file to execute in the chain. There can still be layout pages 221 // and partial pages, but they are never part of the hierarchy. 222 223 // (add server header for falcon debugging) 224 // call to MapPath() is expensive. If we are not emiting source files to header, 225 // don't bother to populate the SourceFiles collection. This saves perf significantly. 226 if (WebPageHttpHandler.ShouldGenerateSourceHeader(Context)) 227 { 228 try 229 { 230 string vp = VirtualPath; 231 if (vp != null) 232 { 233 string path = Context.Request.MapPath(vp); 234 if (!path.IsEmpty()) 235 { 236 PageContext.SourceFiles.Add(path); 237 } 238 } 239 } 240 catch 241 { 242 // we really don't care if this ever fails, so we swallow all exceptions 243 } 244 } 245 246 TemplateStack.Push(Context, this); 247 try 248 { 249 // Execute the developer-written code of the WebPage 250 Execute(); 251 } 252 finally 253 { 254 TemplateStack.Pop(Context); 255 } 256 } 257 InitializePage()258 protected virtual void InitializePage() 259 { 260 } 261 IsSectionDefined(string name)262 public bool IsSectionDefined(string name) 263 { 264 EnsurePageCanBeRequestedDirectly("IsSectionDefined"); 265 return PreviousSectionWriters.ContainsKey(name); 266 } 267 PopContext()268 public void PopContext() 269 { 270 string renderedContent = _tempWriter.ToString(); 271 OutputStack.Pop(); 272 273 if (!String.IsNullOrEmpty(Layout)) 274 { 275 string layoutPagePath = NormalizeLayoutPagePath(Layout); 276 // If a layout file was specified, render it passing our page content 277 OutputStack.Push(_currentWriter); 278 RenderSurrounding( 279 layoutPagePath, 280 w => w.Write(renderedContent)); 281 OutputStack.Pop(); 282 } 283 else 284 { 285 // Otherwise, just render the page 286 _currentWriter.Write(renderedContent); 287 } 288 289 VerifyRenderedBodyOrSections(); 290 SectionWritersStack.Pop(); 291 } 292 PushContext(WebPageContext pageContext, TextWriter writer)293 public void PushContext(WebPageContext pageContext, TextWriter writer) 294 { 295 _currentWriter = writer; 296 PageContext = pageContext; 297 pageContext.Page = this; 298 299 InitializePage(); 300 301 // Create a temporary writer 302 _tempWriter = new StringWriter(CultureInfo.InvariantCulture); 303 304 // Render the page into it 305 OutputStack.Push(_tempWriter); 306 SectionWritersStack.Push(new Dictionary<string, SectionWriter>(StringComparer.OrdinalIgnoreCase)); 307 308 // If the body is defined in the ViewData, remove it and store it on the instance 309 // so that it won't affect rendering of partial pages when they call VerifyRenderedBodyOrSections 310 if (PageContext.BodyAction != null) 311 { 312 _body = PageContext.BodyAction; 313 PageContext.BodyAction = null; 314 } 315 } 316 RenderBody()317 public HelperResult RenderBody() 318 { 319 EnsurePageCanBeRequestedDirectly("RenderBody"); 320 321 if (_renderedBody) 322 { 323 throw new HttpException(WebPageResources.WebPage_RenderBodyAlreadyCalled); 324 } 325 _renderedBody = true; 326 327 // _body should have previously been set in Render(ViewContext,TextWriter) if it 328 // was available in the ViewData. 329 if (_body != null) 330 { 331 return new HelperResult(tw => _body(tw)); 332 } 333 else 334 { 335 throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_CannotRequestDirectly, VirtualPath, "RenderBody")); 336 } 337 } 338 RenderPage(string path, params object[] data)339 public override HelperResult RenderPage(string path, params object[] data) 340 { 341 return RenderPageCore(path, isLayoutPage: false, data: data); 342 } 343 RenderPageCore(string path, bool isLayoutPage, object[] data)344 private HelperResult RenderPageCore(string path, bool isLayoutPage, object[] data) 345 { 346 if (String.IsNullOrEmpty(path)) 347 { 348 throw ExceptionHelper.CreateArgumentNullOrEmptyException("path"); 349 } 350 351 return new HelperResult(writer => 352 { 353 path = NormalizePath(path); 354 WebPageBase subPage = CreatePageFromVirtualPath(path, Context, VirtualPathFactory.Exists, DisplayModeProvider, DisplayMode); 355 var pageContext = CreatePageContextFromParameters(isLayoutPage, data); 356 357 subPage.ConfigurePage(this); 358 subPage.ExecutePageHierarchy(pageContext, writer); 359 }); 360 } 361 RenderSection(string name)362 public HelperResult RenderSection(string name) 363 { 364 return RenderSection(name, required: true); 365 } 366 RenderSection(string name, bool required)367 public HelperResult RenderSection(string name, bool required) 368 { 369 EnsurePageCanBeRequestedDirectly("RenderSection"); 370 371 if (PreviousSectionWriters.ContainsKey(name)) 372 { 373 var result = new HelperResult(tw => 374 { 375 if (_renderedSections.Contains(name)) 376 { 377 throw new HttpException(String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionAleadyRendered, name)); 378 } 379 var body = PreviousSectionWriters[name]; 380 // Since the body can also call RenderSection, we need to temporarily remove 381 // the current sections from the stack. 382 var top = SectionWritersStack.Pop(); 383 384 bool pushed = false; 385 try 386 { 387 if (Output != tw) 388 { 389 OutputStack.Push(tw); 390 pushed = true; 391 } 392 393 body(); 394 } 395 finally 396 { 397 if (pushed) 398 { 399 OutputStack.Pop(); 400 } 401 } 402 SectionWritersStack.Push(top); 403 _renderedSections.Add(name); 404 }); 405 return result; 406 } 407 else if (required) 408 { 409 // If the section is not found, and it is not optional, throw an error. 410 throw new HttpException(String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionNotDefined, name)); 411 } 412 else 413 { 414 // If the section is optional and not found, then don't do anything. 415 return null; 416 } 417 } 418 RenderSurrounding(string partialViewName, Action<TextWriter> body)419 private void RenderSurrounding(string partialViewName, Action<TextWriter> body) 420 { 421 // Save the previous body action and set ours instead. 422 // This value will be retrieved by the sub-page being rendered when it runs 423 // Render(ViewData, TextWriter). 424 var priorValue = PageContext.BodyAction; 425 PageContext.BodyAction = body; 426 427 // Render the layout file 428 Write(RenderPageCore(partialViewName, isLayoutPage: true, data: new object[0])); 429 430 // Restore the state 431 PageContext.BodyAction = priorValue; 432 } 433 434 // Verifies that RenderBody is called, or that RenderSection is called for all sections VerifyRenderedBodyOrSections()435 private void VerifyRenderedBodyOrSections() 436 { 437 // The _body will be set within a layout page because PageContext.BodyAction was set by RenderSurrounding, 438 // which is only called in the case of rendering layout pages. 439 // Using RenderPage will not result in a _body being set in a partial page, thus the following checks for 440 // sections should not apply when RenderPage is called. 441 // Dev10 bug 928341 442 if (_body != null) 443 { 444 if (SectionWritersStack.Count > 1 && PreviousSectionWriters != null && PreviousSectionWriters.Count > 0) 445 { 446 // There are sections defined. Check that all sections have been rendered. 447 StringBuilder sectionsNotRendered = new StringBuilder(); 448 foreach (var name in PreviousSectionWriters.Keys) 449 { 450 if (!_renderedSections.Contains(name)) 451 { 452 if (sectionsNotRendered.Length > 0) 453 { 454 sectionsNotRendered.Append("; "); 455 } 456 sectionsNotRendered.Append(name); 457 } 458 } 459 if (sectionsNotRendered.Length > 0) 460 { 461 throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_SectionsNotRendered, VirtualPath, sectionsNotRendered.ToString())); 462 } 463 } 464 else if (!_renderedBody) 465 { 466 // There are no sections defined, but RenderBody was NOT called. 467 // If a body was defined, then RenderBody should have been called. 468 throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_RenderBodyNotCalled, VirtualPath)); 469 } 470 } 471 } 472 Write(HelperResult result)473 public override void Write(HelperResult result) 474 { 475 WriteTo(Output, result); 476 } 477 Write(object value)478 public override void Write(object value) 479 { 480 WriteTo(Output, value); 481 } 482 WriteLiteral(object value)483 public override void WriteLiteral(object value) 484 { 485 Output.Write(value); 486 } 487 GetOutputWriter()488 protected internal override TextWriter GetOutputWriter() 489 { 490 return Output; 491 } 492 } 493 } 494