Page 1 of 1

ASP.NET - Razor Pages and Core 2. Advanced. 2 of 2

#1 modi123_1   User is online

  • Suitor #2
  • member icon



Reputation: 14097
  • View blog
  • Posts: 56,497
  • Joined: 12-June 08

Post icon  Posted 06 January 2018 - 11:57 AM

Requirements:
Visual Studios 2017 Community or higher.
More than a passing familiarity with C# and SQL

Github:
https://github.com/m...ore2Walkthrough

ASP.NET - Razor Pages, Core 2, and Entity Framework. Basics.
ASP.NET - Razor Pages and Core 2. Advanced. 1 of 2

Welcome back. You should have gone through the Core2 Basics tutorial and the part 1 of this tutorial. If not you may find yourself frustrated and lost so I highly suggest going back through them.

In this section this dives further into more complex areas of database interaction, custom queries, sessions, and mops up with some extra functions that couldn't be reasonably wedged into the website's concept.

Remember - the web page's concept is like a game where a user needs to log in, is presented with some activities, engages those activities, is rewarded, and must wait X minutes until they can repeat the activities. Sounds like Mafia wars, amirite?

Things to remember:
- DB context is your data henchmen and win gman. If there's the need to move data to a database or what not make sure he's there with you.
- A given page's "model" is any set of properties and variables in its code behind.
- If you want to see any user input, and have it survive the journey, flag the property or variable as [BindProperty]

In the same vein as the 'add news' let's shore up adding a user.

6. Registration
Typically web pages have a 'registration' page and this one is no different. We will have a spot for user registration, and some moderate validation to make sure we do not have someone registering the same name multiple times.

Right click the Page's folder -> add Razor page, and call it 'Register'.

Our page is basic and for the tutorial password management will be a topic you need to cover. Definitely do not store passwords plain text in a production database, and at the least salt and hash them.

In the registration page code behind start to build the page's model. Data interaction is needed so we call up our friend 'db context'. Also add an object for our USER class, and a message variable in case we need to tell them to pick a new user name.

Per usual the constructor assigns the db context.
        private readonly AppDbContext _db;

        [TempData]
        public string Message { get; set; }

        [BindProperty] // survive the journey _BACK_ from the user.
        public USERS UserData { get; set; }

        //Constructor
        public RegisterModel(AppDbContext db)
        {
            _db = db;
        }


With model in hand go to the HTML side and add a simple form post with labels for a user name and description. (Description is there for later use with a user's profile page)

<h2>Register</h2>
<h3>@Model.Message</h3>

<form method="post">
    @*<div asp-validation-summary="All"></div>*@
    <div><label asp-for="@Model.UserData.Name"></label>: <input asp-for="UserData.Name" /></div>
    <div><label asp-for="@Model.UserData.DESCRIPTION"></label>: <input asp-for="UserData.DESCRIPTION" /></div>
    <input type="submit" />
</form>


Obviously a more real world page would have things like passwords, emails, and other options, but in the job to keep this simple I wanted to remove kruft best used for later.

Notice since we only have one post the code behind doesn't need a specific name with an 'asp-page-handler='.

Back to the code behind...

Our single, generic, post method begins with validating the model. The temp object will be to catch our return from the query to see if the user name exists already.
        //User clicks a button.
        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            object temp = null;



Strap in - we are going off road. With the news (previous tutoral) we didn't care if it was duplicate or not. Just took what the admin gave and passed the package to the DB context to store. Here we care. No duplicate user names is the 'business requirement' so we need to be more complex with our DB call.

First - ask the db context, politely, for a database connection handle.
            //1.  Check if that user name has been registered.
            var con = _db.Database.GetDbConnection();


Next we tap the connection to open it.

            try
            {
                await con.OpenAsync();



Next, ask the open connection - provided to us by the DB context - for a SQL command object.
                 using (var command = con.CreateCommand())
                {



Fourth, create a SQL string and assign it to the command object's text.

The query is simple. Return '1' if the USERS table has the same as the user is trying to register, and nothing/null if it was not found.

I want this to be case insensitive to I use a SQL 'TOLOWER' to cast what ever is in the name field to all lower case letters.
                    string q = @" SELECT 1 as MyResult
                                  FROM USERS with(nolock)
                                  WHERE TOLOWER(Name) = @userName ";
                    command.CommandText = q;



So far so good, right?

From the command object (provided by the connection object provided by our faithful DB context) we can create a parameter to use in the query.

The parameter name should match our @ in the query above, and the data from our page model is cast to lower as well. (Again, being case insensitive).
                    DbParameter tempParameter = command.CreateParameter();
                    tempParameter.ParameterName = "@userName";
                    tempParameter.Value = UserData.Name.ToLower();
                    command.Parameters.Add(tempParameter);



A bit far in the weeds, but almost done.

Now use the 'execute scalar' which returns the first column's first row (so one cell) of data. Remember this is either 1 or nothing.
https://msdn.microso...(v=vs.110).aspx
                    temp = command.ExecuteScalar();



If the temp object is not null and is equal to 1 then the user tried to register for a name that already exists. We don't want that so we add text to the 'message' object in the pages model and return to our existing page.
                    //2.  If exists then do not do add, and give a message back.
                    if (temp != null && (int)temp == 1)
                    {
                        Message = $"User {UserData.Name} name already exists.";
                        return Page();
                    }



If we are here that means the IF statement wasn't triggered so we loop back around to how we did previous USER adds (or like the NEWS add in the Admin page).

Provide a 'date entered', give the db context the User object, and save the changes.
                    //3.  If not used then do add.
                    UserData.DATE_ENTERED = DateTime.Now;

                    _db.USER_DBSet.Add(UserData);
                    await _db.SaveChangesAsync();

                    Message = $"User {UserData.Name} registered!";
                }
            }
            catch (Exception ex)
            {
                throw;
            }
            finally
            {
                con.Dispose();
            }

            return RedirectToPage("/Index");



You should be able to save this all and run it to verify if a user is being added to a table, and verify what happens when the same name is added twice. An informative foray into more complex queries, but we made it!


6.1 Admin to remove users
If someone can register we may want the admin to remove a user. The ying to the yang if you will.

Head to the previously created 'admin' page. Much like how we wrote up how to remove news we will replicate that for users.

At the top add a collection of users so the model has a handle on it.
        public IList<USERS> UserList { get; private set; }// private set so don't need to have data back.. just to show.


In the 'on get async' have the DB context get the user list.
            UserList = await _db.USER_DBSet.AsNoTracking().ToListAsync();


Flip to the HTML to add a 'form post' to display the users and just like the News we create a button to the user's ID.

    <h3>Users</h3>
    <form method="post">
        <table class="table">
            <thead>
                <tr>
                    @*<th>id</th>*@
                    <th>Name</th>
                    <th>Date Entered</th>
                </tr>
            </thead>

            <tbody>
                @foreach (var temp in Model.UserList)
                {
                    <tr>
                        @*<td>@temp.ID </td>*@
                        <td>@temp.Name</td>
                        <td>@temp.DATE_ENTERED</td>
                        <td>
                                <button type="submit" asp-page-handler="DeleteUser" asp-route-id="@temp.ID">Delete</button>
                        </td>
                    </tr>
                }

            </tbody>

        </table>
    </form>



Back to the code behind create the OnPost Async for 'Delete User'. Nothing crazy here and should look darn similar to the remove news.

        public async Task<IActionResult> OnPostDeleteUserAsync(int id)
        {
            //Find the record to remove.
            var temp = await _db.USER_DBSet.FindAsync(id);

            if (temp != null)
            {
                //Tell the DB context to remove it.
                _db.USER_DBSet.Remove(temp);
                //Save it.
                await _db.SaveChangesAsync();
            }

            Message = $"User deleted.";

            return RedirectToPage();
        }



Users can register and can be removed with only a minor bumpy road into a more complex query to the database. Pat yourself on the back, and grab a cookie. You definitely deserved it.

7. Profiles
With most sites - folks who register have a profile. This could display information from registration, stats, scores, or pictures. (Much like how DIC does it). Our site wouldn't be any different. We can use the user's ID from the database as a way to uniquely pull their profile up.

Start by creating the Razor page per normal (right click Pages -> new razor page -> "Profile").

Per usual we are looking to pull data so DB context needs to be there as well as our USER object to fill.

        private readonly AppDbContext _db;

        [TempData]
        public string Message { get; set; }

        [BindProperty]  // survive the journey _BACK_ from the user.
        public USERS UserData { get; set; }

        //Constructor
        public ProfileModel(AppDbContext db)
        {
            //Get the database context so we can move data to and from the tables.
            _db = db;
        }



The only wrinkle is we want to take any number given on the page's "Get" to search the DB and load any data found. Looks familiar to how the admin page removes users and news.

         public void OnGet(int id)
        {
            var tempUser = _db.USER_DBSet.Find(id);
            UserData = tempUser;
        }        



On the HTML side we can test if the page's model has information or not. If nothing is there (say no id was provided or an invalid id) display one bit of text. If found then display the data.

<h3>@Model.Message</h3>

@if (Model.UserData == null)
{
    <p>No profile to load.</p>
}
else
{
    <p><label asp-for="@Model.UserData.ID"></label> : @Model.UserData.ID</p>
    <p><label asp-for="@Model.UserData.Name"></label> : @Model.UserData.Name</p>
    <p><label asp-for="@Model.UserData.XP"></label> : @Model.UserData.XP</p>
    <p><label asp-for="@Model.UserData.DESCRIPTION"></label> : @Model.UserData.DESCRIPTION</p>


}



You should be able to test this by navigating to your localhost/profile/1 or any number that is valid/invalid for you page.

We will come back to this profile page later, but our work here is done for now.

8. Sessions
This is the major hurdle for this tutorial. Sessions. How to keep track of some states of data between pages while reacting to what is there. In the context of this tutorial sessions are important to see who is an 'admin' and track when someone logs in. It will provide some sweeping changes across the project so buckle up buttercup warp drive is about to be engaged.

Sessions are not on by default and we need to turn them on in the startup services, but we need to add the Nuget package first.

Tools -> Nuget Package Manager -> Manage Nuget Package.

Search for "Microsoft.AspNetCore.Sessions' and install it.

From there head to 'startup.cs. In 'ConfigureServices', and after 'services.addmvc', add the following:

            // Needed for session stuff.  Plus Nuget package.  
            // vvvvvvvvvvvvvvvvv SESSION vvvvvvvvvvvvvvvvvvvvvvvvvv  
            //https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state?tabs=aspnetcore2x
            // Adds a default in-memory implementation of IDistributedCache.
            services.AddDistributedMemoryCache();

            services.AddSession(options =>
            {
                // Set a timeout 
                options.IdleTimeout = TimeSpan.FromSeconds(60);
                options.Cookie.HttpOnly = true;
            });

            //^^/>/>/>/>/>/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/> SESSION ^^/>/>/>/>/>/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^^/>/>/>/>/>^/>^


.. and that's it. Sessions can now 'be a thing' in our project. You can tweak time outs and what not with options while adding more than what is here.

Typically the session values are a key/value pair, and added like this:

HttpContext.Session.SetString("test", "123");



"test" is the key, and "123" is the value.

To fetch a value just ask it to get the string for a given value:

HttpContextAccessor.HttpContext.Session.GetString("test")



A world of possibilities just opened up, and the first to tackle is how to 'log in'.

9. Log in
The biggest use will be tracking when a user appropriately logs into our page. Like a normal site we need to create a 'login' page.

Start by creating the Razor page per normal (right click Pages -> new razor page -> "Login").

Per usual we are looking to pull data so DB context needs to be there as well as our USER object to fill.
        private readonly AppDbContext _db;

        [TempData]
        public string Message { get; set; }// no private set b/c we need data back

        [BindProperty] // survive the journey _BACK_ from the user.
        public USERS UserData { get; set; }

        //Constructor
        public LoginModel(AppDbContext db)
        {
            _db = db;
        }



Since we are not dealing with passwords currently the only log in requirement is the user provides a valid name.

The HTML is pretty simple. Form to post, a user input, and a button to kick off the post.

<h2>Login</h2>

<form method="post">

    <h3>@Model.Message</h3>


    @*<div asp-validation-summary="All"></div>*@
    <div>Name: <input asp-for="UserData.Name" value="test" /></div>
    <input type="submit" />
</form>



The post, on the other handle, is a little bit different than usual.

Using the entity framework in line query we can see if the DB context for USERS has a name in it. To be tricky I think the user better be case exact.

        //User clicks a button.
        public IActionResult OnPost()
        {
            //http://www.entityframeworktutorial.net/Querying-with-EDM.aspx

            // 1.  Using the user name, see if a record can be found.
            var user = from a in _db.USER_DBSet
                       where a.Name == UserData.Name
                       select a;



Fill the object if one is found.

            USERS tempUser = user.FirstOrDefault<USERS>();


Clearly any Session info

           HttpContext.Session.Clear();


If nothing is found tell the user and don't let them log in.

           if (tempUser == null)
            {
                Message = "No User Found";
            }



If they are found then set the name and id in the session.

           else
            {
                // 2.  User is found so stash the name, id, and 'is admin' flag in the session.
                HttpContext.Session.SetString("name", tempUser.Name);
                HttpContext.Session.SetString("id", tempUser.ID.ToString());



If they are admin make a note of that too.

               if (tempUser.IS_ADMIN)
                    HttpContext.Session.SetString("is_admin", "1");



Give a custom welcome message and send them to the index page.

               // 3.  Give a personalized welcome message.
                Message = "Hello " + tempUser.Name + "!";
                
                // 4.  To the index they go.. all logged in.
                return RedirectToPage("/Index");
            }

            return RedirectToPage();



Not terribly bad, but now we can individualize menus and data all for that user!

9.1 Log out
Again with the ying of being able to log in the yang to log out must happen. 'Log out' really means the session information becomes invalid through either timeout or actual user interaction.

In a bid to be fairly universal, and where we will see a bit later, let's stash the single method in the index so head there now.

The method is straight forward. Clear any session, show a message to the user, and redirect to index.

        public IActionResult OnPostLogOut()
        {
            HttpContext.Session.Clear();
            Message = "Logged out";

            return RedirectToPage();
        }



Keep that tucked there for a few sections lower.

9.2 Misc areas
With sessions the website can be tweaked in a few different ways to take that into account.

The first is the admin page. In the HTML side add a line near the top to our HTTPContext

@inject IHttpContextAccessor HttpContextAccessor


A bit below the page name create an if statement to check for a user name and admin check.

If neither are present then simply show a message that there is no access.

@if (HttpContextAccessor.HttpContext.Session.GetString("name") == null && HttpContextAccessor.HttpContext.Session.GetString("is_admin") == null)
{
    <h3>No Access</h3>
}
else
{
}



The rest of the code goes inside the else.. so all the News and User fun and anything in the future we may only want to the admins to see.

While in the admin HTML we should scoot down to the 'Users' area. It occurred to me that an admin should be able to remove everyone but themselves.

Update the submit button to check the ID in the for loop to the logged in user's id. If they are the same do not print a button!

                            @if (HttpContextAccessor.HttpContext.Session.GetString("id") != temp.ID.ToString())
                            {
                                <button type="submit" asp-page-handler="DeleteUser" asp-route-id="@temp.ID">Delete</button>
                            }
                            else
                            {
                                @Html.Raw("&nbsp;");
                            }



Think hard when you add new pages if there needs to be some sort of session check for them.

Make sure to have a test user account with admin flag checked and one that doesn't. See what happens when you try to navigate to the admin pages with no account, a logged into account with the flag, and one that doesn't.

10. Universal layout
There are quite a few files that had not needed interaction with. As you explore this wonderful new playground you may make need of them, but let's focus on one specific one now - the "_Layout.cshtml".

This file is a universal layout that is applied to all the razor pages in the project. Do you have a footer needed on each page? This will have it. Are there site wide CSS you need to load? Put it here! In our case this is where the menu that appears on each page is housed.

Open the file up and dig past the 'body' tag into the first 'nav', the div with the 'container' class, and find the div with the class 'navbar-collapse collapse'. Here you should see an unordered list with our various existing menu bits The plan is we need to think about what menu items should be shown all the time, which are for folk not logged in, which are for folk logged in, and which are for logged in admins.

The thinking is some menu options should disappear with a bit of session checking. For example - if a user logs in do they really need to see the 'register' or 'login' buttons? Most likely not. The same idea with the 'admin' button.. should everyone see it? Most certainly not.

Using the same logic as the sessions section above we can turn off and on things.

After the about page I will put the 'log in' and 'register' buttons, but wrap them in a check to see if there is a session variable. If there is none then that means no one has logged in and show the buttons. If there is don't render them.

                    @if (HttpContextAccessor.HttpContext.Session.GetString("name") == null)
                    {
                        //if not logged in, show these.
                        <li><a asp-page="/Login">Login</a></li>
                        <li><a asp-page="/Register">Register</a></li>
                    }


After that things get a little more fancy. I want a drop down similar to DIC's that is shown when logged in. This will hold a link for the admin, a quick link to the logged in profile, and a logged out button.

Only show if the session variable is set.

                    @if (HttpContextAccessor.HttpContext.Session.GetString("name") != null)
                    {



A nice drop down with a header name 'My Account'.

                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">My Account<span class="caret"></span></a>
                            <ul class="dropdown-menu" role="menu">



If the admin flag is set show the admin else do not render it.

                               @if (HttpContextAccessor.HttpContext.Session.GetString("is_admin") != null)
                                {
                                    //if logged in, AND an admin, show this.
                                    <li><a asp-page="/Admin">Admin</a></li>
                                }



Given the user's logged in ID in session, have a link to the profile page we created. See above for that page.

                               <li><a asp-page="/Profile" asp-route-id="@HttpContextAccessor.HttpContext.Session.GetString("id")">Profile: @HttpContextAccessor.HttpContext.Session.GetString("name")</a></li>



A little wiz bang trickery to have an index button that goes to the index page and looks for the 'logout' method we created above. This works from any page!

                                <li>
                                    <form method="post">
                                        @*Having a hard time making anchor tag be a href and not button*@
                                        @*The Index's code behind has the logic to log out*@
                                        <button type="submit" asp-page="/Index" asp-page-handler="logout">logout</button>
                                    </form>
                                </li>
                            </ul>
                        </li>

                    }


Not bad for a dynamic menu system based off the measlie session we setup earlier. Take another cookie - we are on the home stretch.

11. Activity
The main thrust of the page is to have a user log in and do some activity to gain points and wait for the activity to 'cool down'. This is going to stretch everything you have learned as we go through the sequences of adding a class, a DB, updating DB context, having the admin functions, and show a page to allow the user to do the activity. Buckle in once more and see how what we learned makes this a pretty straight forward addition.

A little more on the concept. The activity will have an XP reward to add to the user's total while a cool down time in minutes to make it more tricky for the user to accrue more points. This means user 1 could kick off an activity and have to wait another 10 minutes while user2 already did the action and has two minutes left. This is dynamic based on teh user who did the action. One possible way is to temporarily log the user id and activity id.. and when the activity page loads only show actions that can be done based on this log table.

11.1 Activity DB.
In the Data folder create two classes 'ACTIVITY' and 'ACTIVITY_LOG'.

Let's throw a curve ball in here and have an 'is active' flag. This means the admin can disable, but not delete, an activity.. say for a special occasion like a holiday or commercial event.

 
    public class ACTIVITY
    {
        public int ID { get; set; }

        [Display(Name = "Title")]
        public string Title { get; set; }

        [Display(Name = "Description")]
        public string Description { get; set; }

        [Display(Name = "XP")]
        public int XP { get; set; }

        [Display(Name = "Cool Down Time")]
        public int? COOL_OFF_MINUTES { get; set; } //in case anyone puts nothing in there.

        public bool IS_ACTIVE { get; set; }

        [Display(Name = "Created Date")]
        public DateTime DATE_ENTERED { get; set; }

        [NotMapped]
        public int? TIME_LEFT { get; set; }

        public ACTIVITY()
        {
            COOL_OFF_MINUTES = 0;
        }
    }



 
    public class ACTIVITY_LOG
    {
        public int ID { get; set; }

        public int ID_USER { get; set; }

        public int ID_ACTIVITY { get; set; }

        public DateTime DATE_ENTERED { get; set; }

    }



Then replicate the tables in the database.

Open up the DB Context and add our DBSets:

 
        public DbSet<ACTIVITY> ACTIVITY_DBSet { get; set; }

        public DbSet<ACTIVITY_LOG> ACTIVITY_LOG_DBSet { get; set; }



.. and in 'OnModelCreating' add the mapping from the classes to the DB tables.

 
            modelBuilder.Entity<ACTIVITY>().ToTable("ACTIVITY");
            modelBuilder.Entity<ACTIVITY_LOG>().ToTable("ACTIVITY_LOG");



Great now let's integrate it into our page.

11.2 Activity Admin.
First we should be able to add/remove data from the table. This means head to the 'admin' page we created earlier.

Much like the users and news add a list of activities to be displayed and a single activity instance for the admin to add new ones.

 
        public IList<ACTIVITY> ActivityList { get; private set; }// private set so don't need to have data back.. just to show.

        [BindProperty]// survive the journey _BACK_ from the user.
        public ACTIVITY ActivityAdd { get; set; }



With the page's model updated head to the HTML side of admin.

Similar to the rest of the display there is a form post method with a table that a for-each fills. The enable/disable button is there to turn off and on activities.

     <form method="post">
        <table class="table">
            <thead>
                <tr>
                    @*<th>ID </th>*@
                    <th>Title</th>
                    <th>XP</th>
                    <th>COOL_OFF_MINUTES</th>
                    <th>IS_ACTIVE</th>
                    <th>DATE_ENTERED</th>
                </tr>
            </thead>

            <tbody>
                @foreach (var temp in Model.ActivityList)
                {
                    <tr>
                        @*<td>@temp.ID </td>*@
                        <td>@temp.Title</td>
                        <td>@temp.XP</td>
                        <td>@temp.COOL_OFF_MINUTES</td>
                        <td>@temp.IS_ACTIVE</td>
                        <td>@temp.DATE_ENTERED</td>
                        <td>
                            <button type="submit" asp-page-handler="ActivityDisable" asp-route-id="@temp.ID">Enable/Disable</button>
                        </td>
                    </tr>
                }

            </tbody>

        </table>
    </form>



Adding activities is a mirror of adding news. Another form post with labels and text boxes to fill out.

     <form method="post">
        <div>
            <label asp-for="ActivityAdd.Title"></label>
            <input asp-for="ActivityAdd.Title" />

            <label asp-for="ActivityAdd.Description"></label>
            <input asp-for="ActivityAdd.Description" />

            <label asp-for="ActivityAdd.XP"></label>
            <input asp-for="ActivityAdd.XP" />

            <label asp-for="ActivityAdd.COOL_OFF_MINUTES"></label>
            <input asp-for="ActivityAdd.COOL_OFF_MINUTES" />

            <label asp-for="ActivityAdd.IS_ACTIVE"></label>
            <input asp-for="ActivityAdd.IS_ACTIVE" />
        </div>
        <input asp-page-handler="ActivityAdd" type="submit" value="Add Activity" />
    </form>



Flip to the code behind to setup the OnPost Async functions needed.

The disable/enable is a quick matter of finding the activity in the DB, getting a handle on it, and flipping the existing 'isActive', and then saving it all back to the DB.

The wrinkle here is after we manually change the row we need to flag it as 'modified' so the DBContext knows to update it.

 
        // the 'id' is from "asp-route-" on the CHTML.  Could have used asp-route-foo, but the parameter would need to be changed to 'int foo'.
        public async Task<IActionResult> OnPostActivityDisableAsync(int id)
        {
            //Find the data row first, before editing can happen.
            var temp = await _db.ACTIVITY_DBSet.FindAsync(id);

            if (temp != null)
            {
                temp.IS_ACTIVE = !temp.IS_ACTIVE;

                //make sure the database context knows there is something to update.
                _db.Attach(temp).State = EntityState.Modified;

                //Have it run the update.
                _db.ACTIVITY_DBSet.Update(temp);
                await _db.SaveChangesAsync();

            }

            Message = $"Activity updated";

            return RedirectToPage("/Admin");
        }



Adding an activity looks about on par with adding news. Nothing out of place here.

        //The user signals there is an action to do.  Notice how the words between "OnPost" and "Async" match up with the CHTML's "asp-page-handler"
        public async Task<IActionResult> OnPostActivityAddAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            ActivityAdd.DATE_ENTERED = DateTime.Now;
            _db.ACTIVITY_DBSet.Add(ActivityAdd);
            await _db.SaveChangesAsync();

            Message = $"Activity added!";

            return RedirectToPage("/Admin");
        }



You can go and test the Admin page's functionality here and see the data show up and/or be disabled.

11.3 Activity Page.
Admin functions out of the way now we focus on how the regular logged in users would interact with the activities. Create a new Razor page called 'activity'.

Per usual we needed our data pipe so add a dbcontext, a list of activities, and set the context in the constructor.

         private readonly AppDbContext _db;

        [TempData]
        public string Message { get; set; }// no private set b/c we need data backl

        // no accessor as this just shows the information recieved before rendering the page.
        public IList<ACTIVITY> ActivityList { get; private set; }

        //Constructor
        public ActivityModel(AppDbContext db)
        {
            //Get the database context so we can move data to and from the tables.
            _db = db;
        }



When the page loads things get a little tricky as we need some moderately complex data back. The page should get any entries in the log table for a given user ID and show how much time each activity has left to 'cool down' while also getting any actvities that the user has not engaged in.. ie not in the log table.

Get the user id from the session, and get a connection from our db context.

         public async Task OnGetAsync()
        {
            int userID = 0;
            int.TryParse(HttpContext.Session.GetString("id"), out userID);

            var con = _db.Database.GetDbConnection();
            try
            {



Open the connection and get a command object from the connection given to us by the db context.

                 await con.OpenAsync();
                using (var command = con.CreateCommand())
                {



Here's the chunk that only gets active activities where there are entries logged in the table. FYI there is a bit of SQL magic happening here, but that is outside the scope of this lengthy tutorial. Nothing a bit of MSDN searching won't clear up.

                     string q = @" SELECT a.ID,
		                        a.TITLE,
		                        a.DESCRIPTION,
		                        a.XP,
		                        a.COOL_OFF_MINUTES,
		                        a.IS_ACTIVE,
		                        a.DATE_ENTERED
                         , IIF(DATEDIFF(SECOND, b.DATE_ENTERED, GETDATE()) < a.cool_off_minutes * 60, a.cool_off_minutes * 60 - DATEDIFF(SECOND, b.DATE_ENTERED, GETDATE()), 0) as TIME_LEFT
                         FROM ACTIVITY a  WITH(NOLOCK)
                         LEFT JOIN ACTIVITY_LOG b  WITH(NOLOCK) on a.ID = b.ID_ACTIVITY
                         WHERE b.ID_USER = @userid 
                            AND a.IS_ACTIVE = 1



Mary that data with all the activities _NOT_ above with a union.

                         UNION

                        SELECT a.ID,
	                        a.TITLE,
	                        a.DESCRIPTION,
	                        a.XP,
	                        a.COOL_OFF_MINUTES,
	                        a.IS_ACTIVE,
	                        a.DATE_ENTERED
	                        , 0 as TIME_LEFT
                        FROM ACTIVITY a WITH(NOLOCK)
                        WHERE a.IS_ACTIVE = 1 
                        and a.ID not in (select z.ID
	                        FROM ACTIVITY z WITH(NOLOCK)
	                        LEFT JOIN ACTIVITY_LOG b  WITH(NOLOCK) on a.ID = b.ID_ACTIVITY
	                        WHERE b.ID_USER = @userid )";
                    command.CommandText = q;



The only parameter needed is the user id.

                     DbParameter tempParameter = command.CreateParameter();
                    tempParameter.ParameterName = "@userid";
                    tempParameter.Value = userID;
                    command.Parameters.Add(tempParameter);


For a fun alternative use a data reader to collect our rows..

                     System.Data.Common.DbDataReader reader = await command.ExecuteReaderAsync();


Assuming the reader has rows, instantiate our list of activitiesi and read each one.

                    if (reader.HasRows)
                    {
                        ActivityList = new List<ACTIVITY>();

                        while (await reader.ReadAsync())
                        {



Create a new activity instance from the reader's data and add it to the list.

                             var row = new ACTIVITY
                            {
                                ID = reader.GetInt32(0),
                                Title = reader.GetString(1),
                                Description = reader.GetString(2),
                                XP = reader.GetInt32(3),
                                COOL_OFF_MINUTES = reader.GetInt32(4),
                                IS_ACTIVE = reader.GetBoolean(5),
                                DATE_ENTERED = reader.GetDateTime(6),
                                TIME_LEFT = reader.GetInt32(7)
                            };
                            ActivityList.Add(row);
                        }
                    }
                    reader.Dispose();
                }



Wrap up the try's catch and now when the page loads the only activities in our page's models are ones that have time left that can't be activated (but we show them this), and ones the user can click on.

             }
            catch (Exception ex)
            {
                throw;
            }
            finally
            {
                con.Dispose();
            }
        }



Crazy, right?

Head to the HTML for a bit more familiar waters. Activities are deeply tied to a user id so only show data on this page if the user has logged in. Don't forget the @inject!

 @inject IHttpContextAccessor HttpContextAccessor

@{
    ViewData["Title"] = "Activity";
}

<h2>Activity</h2>
<h3><font color="lime"> @Model.Message</font></h3>

@* Quick check to see which part of the code is being rendered.*@
@if (HttpContextAccessor.HttpContext.Session.GetString("name") == null)
{
    <h3>No Access</h3>
}
else
{


The table looks fairly normal until we get up to the per-row-action button.

    <form method="post">
        <table class="table">
            <thead>
                <tr>
                    @*<th>ID </th>*@
                    <th>Title</th>
                    <th>XP</th>
                    <th>COOL_OFF_MINUTES</th>
                    <th>IS_ACTIVE</th>
                    <th>DATE_ENTERED</th>
                </tr>
            </thead>

            <tbody>
                @foreach (var temp in Model.ActivityList)
                {
                    <tr>
                        @*<td>@temp.ID </td>*@
                        <td>@temp.Title</td>
                        <td>@temp.XP</td>
                        <td>@temp.COOL_OFF_MINUTES</td>
                        <td>@temp.IS_ACTIVE</td>
                        <td>@temp.DATE_ENTERED</td>
                        <td>



Here there are two possible views.. one if the time left is 0 that means the user can kick off the activity so show an action button with the right page handler and route id of the activity.

                             @if (temp.TIME_LEFT == 0)
                            {
                                //Matches up with the OnPostActivityDoAsync in the cs page.
                                <button type="submit" asp-page-handler="ActivityDo" asp-route-id="@temp.ID">Do</button>
                            }



If the activity still has time left be a gent and show the user how much time is left, but do not have an action button available. Using a bit of C# magic with timespans we can convert the time to minutes and seconds remaining. Refreshing the page should show a lower count each time.

                             else
                            {
                                //Utilizing some .NET ability here.
                                TimeSpan t = TimeSpan.FromSeconds((double)temp.TIME_LEFT);

                                <label>Left: mm: @Math.Floor(@t.TotalMinutes).ToString()   ss: @t.Seconds.ToString() </label>
                            }
                        </td>
                    </tr>
                }

            </tbody>

        </table>
    </form>




This has certainly taken us to the edge of what we have learned and a smidge over. Just one more step on what to do when a user clicks on the action button for an available activity... so back to the code behind.

From the sesion get the user ID.

         public async Task<IActionResult> OnPostActivityDoAsync(int id)
        {
            int userID = 0;

            if (HttpContext.Session.GetString("id") != null)
            {
                int.TryParse(HttpContext.Session.GetString("id"), out userID);



Get the associated XP from the activity's entry in the database and the user's row.

 
                // 1.  Update XP on user profile
                ACTIVITY tempActivity = await _db.ACTIVITY_DBSet.FindAsync(id);
                USERS tempUser = await _db.USER_DBSet.FindAsync(userID);



Add the activityu's XP to the user's total XP, and save it.

                 tempUser.XP += tempActivity.XP;
                _db.USER_DBSet.Update(tempUser);
                _db.SaveChanges();



Next I need to clear out any previous entries of this user id and activity id. For fun this time around we can use LINQ to get a handle to a row in the activity log that has matching keys. (there only should be one at most).

                 // 2.  Upate log.
                ACTIVITY_LOG temp = new ACTIVITY_LOG();

                // If you got this far then the button is enabled so remove all instances of the old.
                var temp2 = _db.ACTIVITY_LOG_DBSet.Where(s => s.ID_ACTIVITY == id && s.ID_USER == userID);



Remove what was found, and save.

                 _db.ACTIVITY_LOG_DBSet.RemoveRange(temp2);
                _db.SaveChanges();


Create a new entry for the table and save that.

                 temp.ID_ACTIVITY = id;

                temp.ID_USER = userID;
                temp.DATE_ENTERED = DateTime.Now;

                _db.ACTIVITY_LOG_DBSet.Add(temp);
                await _db.SaveChangesAsync();



Assuming it all goes well let the player know and head back to the activity table.

             }

            Message = $"Activity started; XP added.";

            return RedirectToPage("/Activity");
        }


Whew! What a load of work. Pat yourself on the back.. that was the final exam! All that is needed now is to update the menu and we can put a bow on activities.


11.4 Activity Menu.
Head to the _layout.cshtml. Right after the 'admin' session check add a link to the activities page.
                                 <li><a asp-page="/Activity">Activity</a></li>


Save it and rejoice! Test your heart out. There are just a few perfuncatory things to mop up and the tutorial should be ready to put in the can.

12. Leader Board

Having XP is great to rack up, but in games like this showing who is top dog is part of the fun. The idea is to have a read only page, open to everyone, to show the rankings. Well within work we have already seen and uses information we already have!

There are no new classes as we will be recyling the user class.

Create a new razor page called 'leaderboard'.

Per usual data means data context and listing things out means a list of users.

         private readonly AppDbContext _db;

        public IList<USERS> UserList { get; private set; }

        public LeaderboardModel(AppDbContext db)
        {
            _db = db;
        }


For a little flair the get will use LINQ to arrange the data by XP and then by name.

         public async Task OnGetAsync()
        {
            UserList = await (from a in _db.USER_DBSet
                       orderby a.XP descending, a.Name
                       select a).ToListAsync();
        }


Boom - the data is ready to load!

Head to the HTML side of life and make a table (no form needed as no user actions can be had) and setup a table per usual. The only curve here is I: opted to use a plain for loop to give each row a numerical count.

 <table class="table">
    <thead>
        <tr>
            <th>#</th>
            <th>Name</th>
            <th>XP</th>
        </tr>
    </thead>

    <tbody>
        @*Make use of a basic for loop to show rankings*@
        @for (int i = 0; i < Model.UserList.Count - 1; i++)
        {
            <tr>
                <td>@(i + 1)</td>
                @*Link to the profiles with the right routing ids *@
                <td><a asp-page="/Profile" asp-route-id="@Model.UserList[i].ID">@Model.UserList[i].Name</a></td>
                <td>@Model.UserList[i].XP</td>
            </tr>
        }

    </tbody>

</table>


Easy peasy.

Since this is page open to everyone go to the _layout.cshtml and add this line right after the 'about'.

                     <li><a asp-page="/Leaderboard">Leader Board</a></li>


13. Miscellaneous
That's it.. we took a voyage and explored the crazy ways to get data, display it, edit it, manipulate it, and organized it into a semi logical page. There are a few last things to clean up that didn't quite fit as neat but nothing out of the norm.

13.1 About page
The about page was left to show a plain jane HTML page. No fancy data loads, saves, or in code curve balls. Yup. you can style this like any HTML page.

13.2 Test page
The test page was created to show case a few other UI elements I couldn't make use of with the expansive, but limited, tutorial.

Here drop down lists, radio buttons, and checkboxes are made use with do nothing data.

13.2.1 Drop Down Lists
Drop down lists have two major componets to their model. A enumerated list of select list items, and a single 'bind property' to get the user's input.

Code behind:

         public IEnumerable<SelectListItem> testDD { get; set; }// DropDown Testing

        [BindProperty]
        public string testDDSelected { get; set; }//DropDown Testing.. gets the selected value on post



The constructor is just there to show a list of three strings.

         public TestModel(AppDbContext db)
        {
            //DropDown Testing - fill values for hte list.
            IList<string> foo = new List<string>();
            foo.Add("aa");
            foo.Add("bb");
            foo.Add("cc");

            //convert that to a 'select list item'.
            testDD = from a in foo
                     select new SelectListItem
                     {
                         Text = a,
                         Value = a
                     };

        }



The action verifies what hte user sent back survived the trip and displays it.

         //DropDown Testing - display selected item to show it was picked.
        public IActionResult OnPostDropDownList()
        {
            Message = "Dropdown: " + testDDSelected;
            return RedirectToPage();
        }



HTML

 <form method="post">
    @* templated helper *@
    @* 1st param - the selected value to return on post, 2nd param the list from the model, 3rd parameter - some initial filler text *@
    @Html.DropDownListFor(a => a.testDDSelected, Model.testDD, "--Select a Value--")
    <button type="submit" asp-page-handler="DropDownList">Test</button>
</form>



13.2.2 Radio buttons
Radio buttons just make use of a variable to hold the value that the user selected.

Code behind:
         [BindProperty]
        public string testRadioButtonselected { get; set; }//Radiobutton Testing.. gets the selected value on post



HTML:

 <form method="post">
    @Html.RadioButtonFor(a => a.testRadioButtonselected, "A")@Html.Label("A")
    @Html.RadioButtonFor(a => a.testRadioButtonselected, "B")@Html.Label("B")
    <button type="submit" asp-page-handler="RadioButton">Test</button>
</form>



13.2.3 Checkboxes
Checkboxes are similar to radio buttons in there is only one object for the page's model which holds the value on return.

         [BindProperty]
        public bool isChecked { get; set; }//Checkbox Testing.. gets the selected value on post



HTML:

 <form method="post">
    @Html.CheckBoxFor(a => a.isChecked)@Html.LabelFor(a => a.isChecked)
    <button type="submit" asp-page-handler="CheckBox">Test</button>
</form>



Wrap up
That took a broad, but sufficiently deep, look at the bulk of functionality you may need in a site while showing varrying ways of doing the same thing. This should get you up on your feet enough to know where to look and what to ask for. Check the github for the complete project.


Areas to expand on:
- text box validation
- actual password salt/hash
- password recovery
- nifty security features and services

Is This A Good Question/Topic? 0
  • +

Replies To: ASP.NET - Razor Pages and Core 2. Advanced. 2 of 2

#2 modi123_1   User is online

  • Suitor #2
  • member icon



Reputation: 14097
  • View blog
  • Posts: 56,497
  • Joined: 12-June 08

Posted 06 January 2018 - 12:07 PM

What things would look like in the end..


Posted Image

Posted Image

Posted Image

Posted Image

Posted Image

Posted Image

Posted Image

Posted Image

Posted Image
Was This Post Helpful? 0
  • +
  • -

#3 modi123_1   User is online

  • Suitor #2
  • member icon



Reputation: 14097
  • View blog
  • Posts: 56,497
  • Joined: 12-June 08

Posted 10 February 2018 - 01:03 PM

Added to the git code base after the fact:

- Dropdown list with AJAX call to function
Core2Walkthrough -> Pages -> Test

- Testing the uploading of files.
Core2Walkthrough -> Pages -> TestUpload
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1