File: System\Data\WebControls\Design\EntityDataSourceDataSelection.cs
Project: ndp\fx\src\DataWebControlsDesign\System.Web.Entity.Design.csproj (System.Web.Entity.Design)
//------------------------------------------------------------------------------
// <copyright file="EntityDataSourceDataSelection.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//
// @owner       Microsoft
// @backupOwner Microsoft
//
// Manages the properties that can be set on the second page of the wizard
//------------------------------------------------------------------------------
 
namespace System.Web.UI.Design.WebControls
{
    using System.Collections.Generic;
    using System.Data.Metadata.Edm;
    using System.Diagnostics;
    using System.Globalization;
    using System.Text;
 
    internal class EntityDataSourceDataSelection
    {
        #region Private static fields
        // iterator prefix used in building and parsing Select property value
        private static readonly string s_itKeyword = "it.";
        // Placeholder item to indicate (None) on the EntityTypeFilter ComboBox
        private static readonly EntityDataSourceEntityTypeFilterItem s_entityTypeFilterNoneItem =
                new EntityDataSourceEntityTypeFilterItem(Strings.Wizard_DataSelectionPanel_NoEntityTypeFilter);
        #endregion
 
        #region Private readonly fields
        private readonly EntityDataSourceDataSelectionPanel _panel;
        private readonly EntityDataSourceDesignerHelper _helper;
        #endregion
 
        #region Private fields for temporary storage of property values
        private readonly EntityDataSourceState _entityDataSourceState;
        private List<EntityDataSourceEntitySetNameItem> _entitySetNames;
        private EntityDataSourceEntitySetNameItem _selectedEntitySetName;
        private List<EntityDataSourceEntityTypeFilterItem> _entityTypeFilters;
        private EntityDataSourceEntityTypeFilterItem _selectedEntityTypeFilter;
 
        #region Select views
        // The Data Selection wizard panel can display two kinds of views of the Select property:
        //     (1) Simple Select View: CheckedListBox with a list of available entity type properties
        //     (2) Advanced Select View: TextBox that allows any statement to be entered (no validation)
        //
        // When either view is visible to the user, the fields shown below for that view should be non-null, and the fields
        // for the other view should be null.
 
        // Simple Select View
        // _selectedEntityTypeProperties contains a set of indexes of properties in _entityTypeProperties
        private List<string> _entityTypeProperties; 
        private List<int> _selectedEntityTypeProperties;
 
        // Advanced Select View
        private string _select;
        #endregion
 
        private bool _enableInsert;
        private bool _enableUpdate;
        private bool _enableDelete;
        private readonly EntityDataSourceWizardForm _wizardForm;        
        #endregion
 
        #region Constructors
        internal EntityDataSourceDataSelection(EntityDataSourceDataSelectionPanel panel, EntityDataSourceWizardForm wizard, EntityDataSourceDesignerHelper designerHelper, EntityDataSourceState entityDataSourceState)
        {
            _panel = panel;
            _panel.Register(this);
            _helper = designerHelper;
 
            _entityDataSourceState = entityDataSourceState;
            _wizardForm = wizard;
        }
        #endregion
 
        #region Events
        // Event handler to process notifications when a DefaultContainerName is selected on the ObjectContext configuration panel
        internal void ContainerNameChangedHandler(object sender, EntityDataSourceContainerNameItem newContainerName)
        {
            // Load the entity sets for this container, don't select anything initially in the list
            LoadEntitySetNames(newContainerName, null);
            
            // Reset the other controls that depend on the value of EntitySet
            LoadEntityTypeFilters(null, null);
            LoadSelect(String.Empty);
        }
        #endregion        
 
        #region Methods to manage temporary state and wizard contents
        // Used when the wizard is launched, to load existing property values from data source control
        internal void LoadState()
        {
            LoadEntitySetNames(_helper.GetEntityContainerItem(_entityDataSourceState.DefaultContainerName), _entityDataSourceState.EntitySetName);
            LoadEntityTypeFilters(_selectedEntitySetName, _entityDataSourceState.EntityTypeFilter);
            LoadSelect(_entityDataSourceState.Select);
            LoadInsertUpdateDelete();
        }
 
        // Save current wizard settings back to the EntityDataSourceState
        internal void SaveState()
        {
            SaveEntitySetName();
            SaveEntityTypeFilter();
            SaveSelect();
            SaveInsertUpdateDelete();
            SaveEnableFlattening();
        }
 
        #region EntitySetName
        // Find the specified entitySetName in the list or add it if it's not there
        private EntityDataSourceEntitySetNameItem FindEntitySetName(string entitySetName)
        {
            if (!String.IsNullOrEmpty(entitySetName))
            {
                EntityDataSourceEntitySetNameItem entitySetToSelect = null;
                foreach (EntityDataSourceEntitySetNameItem entitySetNameItem in _entitySetNames)
                {
                    // Ignore case here when searching the list for a matching item, but set the temporary state property to the
                    // correctly-cased version from metadata so that if the user clicks Finish, the correct one will be saved. This
                    // allows some flexibility the designer without preserving an incorrectly-cased value that could cause errors at runtime.                    
                    if (String.Equals(entitySetNameItem.EntitySetName, entitySetName, StringComparison.OrdinalIgnoreCase))
                    {
                        entitySetToSelect = entitySetNameItem;
                    }
                }
 
                // didn't find a matching entityset, so just create a placeholder for one using the specified name and add it to the list
                if (entitySetToSelect == null)
                {
                    entitySetToSelect = new EntityDataSourceEntitySetNameItem(entitySetName);
                    _entitySetNames.Add(entitySetToSelect);                    
                }
 
                Debug.Assert(entitySetToSelect != null, "expected a non-null EntityDataSourceEntitySetNameItem");
                return entitySetToSelect;
            }
 
            return null;
        }
 
        // Populates the EntitySetName combobox with all of the discoverable EntitySets for the specified container.
        // If the specified entitySetName is not empty, it is added to the list and selected as the initial value
        // containerNameItem may not be backed by a real EntityContainer, in which case there is no way to look up the EntitySet in metadata
        // devnote: This method should not automatically reset EntityTypeFilter and Select because it can be used to load the initial state
        //          for the form, in which case we need to preserve any values that are already set on the data source control.
        private void LoadEntitySetNames(EntityDataSourceContainerNameItem containerNameItem, string entitySetName)
        {
            // If this is a container that we found in the project's metadata, get a list of EntitySets for that container
            if (containerNameItem != null && containerNameItem.EntityContainer != null)
            {
                _entitySetNames = _helper.GetEntitySets(containerNameItem.EntityContainer, false /*sortResults*/);
 
                // Try to find the specified entityset in list and add it if it isn't there
                _selectedEntitySetName = FindEntitySetName(entitySetName);
            }
            else
            {
                // if this is an unknown container, there is no way to find a list of entitysets from metadata
                // so just create a new list and placeholder for the specified entityset
                _entitySetNames = new List<EntityDataSourceEntitySetNameItem>();
                if (!String.IsNullOrEmpty(entitySetName))
                {
                    _selectedEntitySetName = new EntityDataSourceEntitySetNameItem(entitySetName);
                    _entitySetNames.Add(_selectedEntitySetName);
                }
                else
                {
                    _selectedEntitySetName = null;
                }
            }
 
            // Sort the list now, after we may have added one above
            _entitySetNames.Sort();
 
            // Update the controls
            _panel.SetEntitySetNames(_entitySetNames);
            _panel.SetSelectedEntitySetName(_selectedEntitySetName);
        }
 
        // Set EntitySetName in temporary storage
        internal void SelectEntitySetName(EntityDataSourceEntitySetNameItem selectedEntitySet)
        {
            _selectedEntitySetName = selectedEntitySet;
            // Load the types for the selected EntitySet, don't select one initially
            LoadEntityTypeFilters(selectedEntitySet, null);
            // Reinitialize the Select control with a list of properties, don't preserve any existing Select value
            LoadSelect(String.Empty);
        }
 
        private void SaveEntitySetName()
        {
            if (_selectedEntitySetName != null)
            {
                _entityDataSourceState.EntitySetName = _selectedEntitySetName.EntitySetName;
            }
            else
            {
                _entityDataSourceState.EntitySetName = String.Empty;
            }
        }
        #endregion
 
        #region EntityTypeFilter
        // Populate a list with the base type for the EntitySet plus all derived types, and a special entry to indicate no filter
        // devnote: This method should not automatically reset Select because it can be used to load the initial state
        //          for the form, in which case we need to preserve any values that are already set on the data source control.
        private void LoadEntityTypeFilters(EntityDataSourceEntitySetNameItem entitySetItem, string entityTypeFilter)
        {
            // If this is an EntitySet that we found in the project's metadata, get the type information
            if (entitySetItem != null && entitySetItem.EntitySet != null)
            {
                _entityTypeFilters = _helper.GetEntityTypeFilters(entitySetItem.EntitySet.ElementType, false /*sortResults*/);
                // add (None) to the beginning of the list
                _entityTypeFilters.Insert(0, s_entityTypeFilterNoneItem);
 
                // Try to find the specified type in list and add it if it isn't there
                _selectedEntityTypeFilter = FindEntityTypeFilter(entityTypeFilter);
            }
            else
            {
                // if this is an unknown EntitySet, there is no way to find a list of types from metadata
                // so just create a new list and placeholder for the specified type
                _entityTypeFilters = new List<EntityDataSourceEntityTypeFilterItem>();
                _entityTypeFilters.Add(s_entityTypeFilterNoneItem);
 
                if (!String.IsNullOrEmpty(entityTypeFilter))
                {
                    _selectedEntityTypeFilter = new EntityDataSourceEntityTypeFilterItem(entityTypeFilter);
                    _entityTypeFilters.Add(_selectedEntityTypeFilter);
                }
                else
                {
                    _selectedEntityTypeFilter = s_entityTypeFilterNoneItem;
                }
            }
 
            // Sort now after we might have added items above
            _entityTypeFilters.Sort();
 
            // Update the controls
            _panel.SetEntityTypeFilters(_entityTypeFilters);
            _panel.SetSelectedEntityTypeFilter(_selectedEntityTypeFilter);
        }
 
        // Find the specified entityTypeFilter in the list and add it if it's not there
        private EntityDataSourceEntityTypeFilterItem FindEntityTypeFilter(string entityTypeFilter)
        {
            if (!String.IsNullOrEmpty(entityTypeFilter))
            {
                EntityDataSourceEntityTypeFilterItem typeToSelect = null;
                foreach (EntityDataSourceEntityTypeFilterItem entityTypeFilterItem in _entityTypeFilters)
                {
                    // Ignore case here when searching the list for a matching item, but set the temporary state property to the
                    // correctly-cased version from metadata so that if the user clicks Finish, the correct one will be saved. This
                    // allows some flexibility the designer without preserving an incorrectly-cased value that could cause errors at runtime.                    
                    if (String.Equals(entityTypeFilterItem.EntityTypeName, entityTypeFilter, StringComparison.OrdinalIgnoreCase))
                    {
                        typeToSelect = entityTypeFilterItem;
                    }
                }
 
                // didn't find a matching type, so just create a placeholder item and add it to the list
                if (typeToSelect == null)
                {
                    typeToSelect = new EntityDataSourceEntityTypeFilterItem(entityTypeFilter);
                    _entityTypeFilters.Add(typeToSelect);
 
                }
 
                Debug.Assert(typeToSelect != null, "expected a non-null string for EntityTypeFilter");
                return typeToSelect;
            }
 
            return s_entityTypeFilterNoneItem;
        }
 
        // Set EntityTypeFilter in temporary storage and load the Select property
        internal void SelectEntityTypeFilter(EntityDataSourceEntityTypeFilterItem selectedEntityTypeFilter)
        {
            _selectedEntityTypeFilter = selectedEntityTypeFilter;
            // Reinitialize the Select control with a list of properties, don't preserve any existing Select value
            LoadSelect(String.Empty);
        }
 
        private void SaveEntityTypeFilter()
        {
            // If (None) is selected, it is the same as an empty string on the data source control
            if (Object.ReferenceEquals(_selectedEntityTypeFilter, s_entityTypeFilterNoneItem))
            {
                _entityDataSourceState.EntityTypeFilter = String.Empty;
            }
            else
            {
                _entityDataSourceState.EntityTypeFilter = _selectedEntityTypeFilter.EntityTypeName;
            }
        }
 
        #endregion
 
        #region Select
        // Load and parse the Select property
        private void LoadSelect(string select)
        {
            Debug.Assert(_selectedEntityTypeFilter != null, "_selectedEntityTypeFilter should never be null");
 
            EntityType entityType = GetSelectedEntityType();
 
            if (entityType != null)
            {
                // this is a real type from metadata, load its properties
                _entityTypeProperties = _helper.GetEntityTypeProperties(entityType);
                // add the 'Select All (Entity Value)' placeholder at the beginning of the list
                _entityTypeProperties.Insert(0, Strings.Wizard_DataSelectionPanel_SelectAllProperties);
 
                // parse the current value for the Select property to see if it can be displayed in the simple CheckedListBox view                
                if (TryParseSelect(select))
                {
                    _select = null;
 
                    // Update the controls
                    _panel.SetEntityTypeProperties(_entityTypeProperties, _selectedEntityTypeProperties);
                    UpdateInsertUpdateDeleteState();
                    return;
                }
                // else we failed to parse the select into entity type properties on the specified type, so just use the advanced select view
            } // else can't get a list of properties unless we have a known EntityType
 
 
            // if we don't have a valid entity type or couldn't parse the incoming Select value, just display the advanced TextBox view
            _entityTypeProperties = null;
            _selectedEntityTypeProperties = null;
            _select = select;
 
            // Update the controls
            _panel.SetSelect(_select);
            UpdateInsertUpdateDeleteState();
        }
 
        // Build a value for the Select property from the selected values in the CheckedListBox
        // Value will be in the from "it.Property1, it.Property2, it.Property3"
        private string BuildSelect()
        {
            Debug.Assert(_selectedEntityTypeProperties != null && _selectedEntityTypeProperties.Count > 0, "expected non-null _selectedEntityTypeProperties with at least one value");
 
            // 'Select All (Entity Value)' is the same thing as an empty string for the property
            if (_selectedEntityTypeProperties[0] == 0)
            {
                Debug.Assert(_selectedEntityTypeProperties.Count == 1, "'Select All (Entity Value)' should be the only property selected");
                return String.Empty;
            }
 
            StringBuilder selectProperties = new StringBuilder();
            bool addComma = false;
            foreach (int propertyIndex in _selectedEntityTypeProperties)
            {
                if (addComma)
                {
                    selectProperties.Append(", ");
                }
                else
                {
                    addComma = true;
                }
 
                selectProperties.AppendFormat(CultureInfo.InvariantCulture, "{0}{1}", s_itKeyword, EscapePropertyName(_entityTypeProperties[propertyIndex]));
 
            }
            return selectProperties.ToString();
        }
 
        private static string EscapePropertyName(string propertyName)
        {
            return "[" + propertyName.Replace("]", "]]") + "]";
        }
 
        static string UnescapePropertyName(string name)
        {
            if (name[0] == '[' && name[name.Length - 1] == ']')
            {
                return name.Substring(1, name.Length - 2).Replace("]]", "]");
            }
            else
            {
                // else the property is not escaped at all or is not properly escaped. We can't parse it so just return.
                return name;
            }            
        }
 
        // Parses the current Select property on the data source to see if it matches a specific format that we can use to display the properties
        // in the CheckedListBox in the simple select wizard view
        private bool TryParseSelect(string currentSelect)
        {
            bool parseSuccess = false; // gets set to true after the statement has been successfully parsed
            if (!String.IsNullOrEmpty(currentSelect))
            {
                // first try to split the string up into pieces divided by commas
                // expects a format like the following: (extra spaces around the commas should work as well)
                //     "it.KnownPropertyName1, it.KnownPropertyName2, it.KnownPropertyName3"                
                string[] tokenizedSelect = currentSelect.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
 
                bool foundUnknownProperty = false;
                List<int> selectedProperties = new List<int>();
                foreach (string token in tokenizedSelect)
                {
                    string propertyName = token.Trim();
 
                    // Does the current property token start with "it."?
                    if (ReadItKeyword(propertyName))
                    {
                        // Does the rest of the property token match a known property name for the selected EntityTypeFilter?
                        int propertyIndex = ReadPropertyName(propertyName.Substring(s_itKeyword.Length));
                        if (propertyIndex == -1)
                        {
                            // the property was not known, so we can just stop looking
                            foundUnknownProperty = true;
                            break;
                        }
                        else
                        {
                            // this is a known property, so add its index to the list
                            selectedProperties.Add(propertyIndex);
                        }
                    }
                    else
                    {
                        // the property was not known, so we can just stop looking
                        foundUnknownProperty = true;
                        break;
                    }
                }
                if (!foundUnknownProperty)
                {
                    // if we never found anything unknown, the current list of properties is what we'll use to fill in the CheckedListBox
                    _selectedEntityTypeProperties = selectedProperties;
                    parseSuccess = true;
                }
                else
                {
                    _selectedEntityTypeProperties = null;
                }
            }
            else
            {
                // if Select is empty, we just want to add 'Select All (Entity Value)' to the list
                _selectedEntityTypeProperties = new List<int>();
                _selectedEntityTypeProperties.Add(0);
                parseSuccess = true;
            }
 
            return parseSuccess;
        }
 
        // Determines if the specified propertyName starts with "it." (case-insensitive)
        private bool ReadItKeyword(string propertyName)
        {
            // will accept any casing of "it." here, although when the value is saved back to the property, it will be correctly lower-cased
            return propertyName.StartsWith(s_itKeyword, StringComparison.OrdinalIgnoreCase);
        }
 
        // Determines if the specified propertyName matches one of the known properties for the selected type
        private int ReadPropertyName(string propertyName)
        {
            for (int propIndex = 0; propIndex < _entityTypeProperties.Count; propIndex++)
            {
                // Ignore case here when searching the list for a matching item, but set the temporary state property to the
                // correctly-cased version from metadata so that if the user clicks Finish, the correct one will be saved. This
                // allows some flexibility the designer without preserving an incorrectly-cased value that could cause errors at runtime.
 
                // Does the specified property name exactly match any of the properties for the selected EntityTypeFilter?
                if (String.Equals(UnescapePropertyName(propertyName), _entityTypeProperties[propIndex], StringComparison.OrdinalIgnoreCase))
                {
                    return propIndex;
                }
            }
 
            return -1;
        }
 
        // Add the specified property to the list of selected entity properties used to build up the Select property
        internal void SelectEntityProperty(int propertyIndex)
        {
            _selectedEntityTypeProperties.Add(propertyIndex);
        }
 
        internal void ClearAllSelectedProperties()
        {
            _selectedEntityTypeProperties.Clear();
        }
 
        // Remove specified entity property index from the selected list
        internal void DeselectEntityProperty(int propertyIndex)
        {
            _selectedEntityTypeProperties.Remove(propertyIndex);
        }
 
        // Set Select property to the specified string (used with advanced select view)
        internal void SelectAdvancedSelect(string select)
        {
            _select = select;
        }
 
        private void SaveSelect()
        {
            if (_select != null)
            {
                _entityDataSourceState.Select = _select;
            }
            else
            {
                Debug.Assert(_selectedEntityTypeProperties != null, "expected _entityTypeProperties to be non-null if _select is null");
                _entityDataSourceState.Select = BuildSelect();
            }
        }
        #endregion
 
        #region EnableInsertUpdateDelete
        // Load the initial values for EnableInsert/EnableUpdate/EnableDelete CheckBoxes
        private void LoadInsertUpdateDelete()
        {
            SelectEnableInsert(_entityDataSourceState.EnableInsert);
            SelectEnableUpdate(_entityDataSourceState.EnableUpdate);
            SelectEnableDelete(_entityDataSourceState.EnableDelete);
 
            UpdateInsertUpdateDeleteState();
        }
 
        // Set EnableDelete in temporary storage
        internal void SelectEnableDelete(bool enableDelete)
        {
            _enableDelete = enableDelete;
        }
 
        // Set EnableInsert in temporary storage
        internal void SelectEnableInsert(bool enableInsert)
        {
            _enableInsert = enableInsert;
        }
 
        // Set EnableUpdate in temporary storage
        internal void SelectEnableUpdate(bool enableUpdate)
        {
            _enableUpdate = enableUpdate;
        }
 
        private void SaveInsertUpdateDelete()
        {
            _entityDataSourceState.EnableInsert = _enableInsert;
            _entityDataSourceState.EnableUpdate = _enableUpdate;
            _entityDataSourceState.EnableDelete = _enableDelete;
        }
 
        /// <summary>
        /// Update the panel control state based on the valued of enableInsert,
        /// enableUpdate, enableDelete, and the selectedEntityTypeProperties
        /// </summary>
        internal void UpdateInsertUpdateDeleteState()
        {
            // Set the checkbox state for the panel controls
            _panel.SetEnableInsertUpdateDelete(_enableInsert, _enableUpdate, _enableDelete);
 
            // The InsertUpdateDelete panel should be enabled if:
            // 1. Insert, Update, or Delete is selected -OR-
            // 2. The EntitySelection has SelectAll checked
            bool enablePanel = (_enableInsert || _enableUpdate || _enableDelete || 
                        (_selectedEntityTypeProperties != null && 
                         _selectedEntityTypeProperties.Count == 1 && 
                         _selectedEntityTypeProperties[0] == 0));
 
            _panel.SetEnableInsertUpdateDeletePanel(enablePanel);
        }
        #endregion        
 
        #region  EnableFlattening
 
        private EntityType GetSelectedEntityType()
        {
            EntityType entityType = null;
 
            // determine which EntityType to load properties for, based on the value selected for EntityTypeFilter
            if (Object.ReferenceEquals(_selectedEntityTypeFilter, s_entityTypeFilterNoneItem))
            {
                // If (None) is selected, use the base type for the EntitySet if available
                if (_selectedEntitySetName != null && _selectedEntitySetName.EntitySet != null)
                {
                    entityType = _selectedEntitySetName.EntitySet.ElementType;
                }
                // else the EntitySet base type is not known
            }
            else
            {
                entityType = _selectedEntityTypeFilter.EntityType; // could still be null if the type if not known in metadata
            }
 
            return entityType;
        }
 
        private void SaveEnableFlattening()
        {
            bool enableFlattening = false;
 
            EntityType entityType = GetSelectedEntityType();
 
            if (entityType != null)
            {
                foreach (EdmMember member in entityType.Members)
                {
                    // If there is a complex member, enable flattening
                    if (member.TypeUsage.EdmType.BuiltInTypeKind == BuiltInTypeKind.ComplexType)
                    {
                        enableFlattening = true;
                        break;
                    }
                    else if (member.BuiltInTypeKind == BuiltInTypeKind.NavigationProperty)
                    {
                        NavigationProperty navProp = (NavigationProperty)member;
                        if (navProp.ToEndMember.RelationshipMultiplicity != RelationshipMultiplicity.Many)
                        {
                            AssociationType associationType = navProp.ToEndMember.DeclaringType as AssociationType;
                            if (!associationType.IsForeignKey)
                            {
                                // If there is an independent association, enable flattening
                                enableFlattening = true;
                                break;
                            }
                        }
                    }
                }
            }
            else
            {
                // Projection
                enableFlattening = true;
            }
 
            _entityDataSourceState.EnableFlattening = enableFlattening;
        }
 
        #endregion
 
        #endregion
 
        #region Wizard button state management
        internal void UpdateWizardState()
        {
            // EntitySetName must be selected and a Select must be configured or must be the empty string
            _wizardForm.SetCanFinish(_selectedEntitySetName != null && (_select != null || _selectedEntityTypeProperties.Count > 0));
        }
        #endregion
    }
}